Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports = {
"no-constant-condition": ["error", {'checkLoops': false}],
"brace-style": ["error", "1tbs", {"allowSingleLine": true}],
"block-spacing": ["error", "always"],
"no-multiple-spaces": ["error", {"ignoreEOLComments": true}],
},
"ignorePatterns": ["js/lib/*"],

Expand Down
3 changes: 2 additions & 1 deletion js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import pink from '@material-ui/core/colors/pink';
import {HashRouter as Router, Switch, Route, Redirect} from 'react-router-dom';

import {useScrollToAnchor} from './ScrollToAnchorHelper';
import {ANM_INS_TABLE, ANM_VAR_TABLE, STD_TABLE, MSG_TABLE} from './tables';
import {ANM_INS_TABLE, ANM_VAR_TABLE, STD_TABLE, MSG_TABLE, END_TABLE} from './tables';
import {CurrentPageProvider} from './UrlTools';
import {BackgroundProvider, useDarkBg} from './Background';
import {ErrorBoundary} from './Error';
Expand Down Expand Up @@ -117,6 +117,7 @@ function Content({setContentLoaded}: {setContentLoaded: React.Dispatch<boolean>}
<Route exact path="/anm/var"><ReferenceTablePage table={ANM_VAR_TABLE} setContentLoaded={setContentLoaded} /></Route>
<Route exact path="/std/ins"><ReferenceTablePage table={STD_TABLE} setContentLoaded={setContentLoaded} /></Route>
<Route exact path="/msg/ins"><ReferenceTablePage table={MSG_TABLE} setContentLoaded={setContentLoaded} /></Route>
<Route exact path="/end/ins"><ReferenceTablePage table={END_TABLE} setContentLoaded={setContentLoaded} /></Route>
<Route exact path="/anm/stats"><StatsPage /></Route>
<Route exact path="/anm/layer-viewer"><LayerViewerPage /></Route>

Expand Down
8 changes: 8 additions & 0 deletions js/InlineRef.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import {Err} from './Error';
export const CurrentReferenceTableRowContext = React.createContext<Ref | null>(null);

export function InlineRef({r}: {r: string}) {
// Second layer of protection: catch undefined refs
if (!r || typeof r !== 'string') {
throw new Error(`
FATAL: InlineRef received invalid ref value: ${JSON.stringify(r)}.
This likely means you used :ref[xxx:yyy] instead of :ref{r=xxx:yyy}.
The colon in square brackets gets parsed as a nested directive!
`);
}
const nameSettings = useNameSettings();
const refData = useRefData(r);
if (!refData) {
Expand Down
15 changes: 14 additions & 1 deletion js/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,20 @@ const makeGc = (Component: ((p: {game: Game}) => ReactElement)) => {
};

type RefProps = {tip?: string, url?: string};
function Ref({r, ...props}: {r: string} & RefProps) {
function Ref({r, children, ...props}: {r?: string, children?: ReactNode} & RefProps) {
// It is extremely likely that I will accidentally write ":ref[]" in the future instead of the attribute syntax.
if (!r || typeof r !== 'string') {
if (children) {
// If we have children but no r, it means :ref[something] was used instead of :ref{r=something}
throw new Error(
`SYNTAX ERROR: Found :ref[] with children but no 'r' prop. ` +
`You probably wrote :ref[lang:name] but should have written :ref{r=lang:name}. ` +
`The colon inside [...] gets parsed as a nested directive!`
);
} else {
throw new Error(`SYNTAX ERROR: :ref directive is missing the required 'r' attribute. Use :ref{r=lang:name} syntax.`);
}
}
const allProps = {tip: "1", url: "1", ...props};
const tip = allProps.tip === "1";
const url = allProps.url === "1";
Expand Down
1 change: 1 addition & 0 deletions js/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const NAVBAR: Entry[] = [
{url: "#/anm/var", label: "ANM variables"},
{url: "#/std/ins", label: "STD instructions"},
{url: "#/msg/ins", label: "MSG instructions"},
{url: "#/end/ins", label: "END instructions"},
{
label: "Tools",
children: [
Expand Down
20 changes: 18 additions & 2 deletions js/ReferenceTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ import {VarHeader, InsSiggy} from './InsAndVar';

export function ReferenceTablePage<D extends CommonData>({table, setContentLoaded}: {table: TableDef<D>, setContentLoaded: (x: boolean) => void}) {
const currentGame = useCurrentPageGame();
const history = useHistory();

// If the current game isn't supported by this table, redirect to the latest supported game
React.useEffect(() => {
const supportedGames = table.supportedGames();
if (!supportedGames.includes(currentGame)) {
const latestSupportedGame = supportedGames[supportedGames.length - 1];
history.replace({search: `?g=${latestSupportedGame}`});
}
}, [currentGame, table, history]);

return <>
<p>
{"Select game version: "}
Expand Down Expand Up @@ -111,10 +122,15 @@ function ReferenceTable<D extends CommonData>({table, currentGame: game, setCont


function getInstrCounts<D extends CommonData>(table: TableDef<D>, game: Game) {
const map = table.refByOpcode.get(game)!;
const map = table.refByOpcode.get(game);

// If this game isn't supported by the table, return zeros
if (!map) {
return {total: 0, documented: 0};
}

let documented = 0;
for (const opcode of table.refByOpcode.get(game)!.keys()) {
for (const opcode of map.keys()) {
const refObj = table.getRefByOpcode(game, opcode)!;
const data = table.getDataByRef(refObj.ref);
if (!data) {
Expand Down
2 changes: 2 additions & 0 deletions js/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export function SettingsPage({settings, onSave}: {settings: SavedSettings, onSav
<SingleLangSettings state={state.std} dispatch={dispatch.std} />
<h2>msgmap</h2>
<SingleLangSettings state={state.msg} dispatch={dispatch.msg} />
<h2>endmap</h2>
<SingleLangSettings state={state.end} dispatch={dispatch.end} />
</>;
}

Expand Down
3 changes: 3 additions & 0 deletions js/settings/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function getSavedSettingsFromLocalStorage(): SavedSettings {
anm: fromLangV0('anm'),
std: fromLangV0('std'),
msg: defaultLangSettings(),
end: defaultLangSettings(),
};
}

Expand All @@ -56,6 +57,7 @@ export function getSavedSettingsFromLocalStorage(): SavedSettings {
anm: defaultLangSettings(),
msg: defaultLangSettings(),
std: defaultLangSettings(),
end: defaultLangSettings(),
};
}
}
Expand All @@ -82,6 +84,7 @@ function deserializeSettingsFromString(json: string) {
anm: data.anm ? parseSavedLangSettingsV1(data.anm) : defaultLangSettings(),
msg: data.msg ? parseSavedLangSettingsV1(data.msg) : defaultLangSettings(),
std: data.std ? parseSavedLangSettingsV1(data.std) : defaultLangSettings(),
end: data.end ? parseSavedLangSettingsV1(data.end) : defaultLangSettings(),
};
}

Expand Down
3 changes: 2 additions & 1 deletion js/settings/settings-page-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export function useSettingsPageStateReducer(settings: SavedSettings): [State, Di
const [anmState, anmDispatch] = useLangStateReducer(settings, 'anm');
const [stdState, stdDispatch] = useLangStateReducer(settings, 'std');
const [msgState, msgDispatch] = useLangStateReducer(settings, 'msg');
return [{anm: anmState, std: stdState, msg: msgState}, {anm: anmDispatch, std: stdDispatch, msg: msgDispatch}];
const [endState, endDispatch] = useLangStateReducer(settings, 'end');
return [{anm: anmState, std: stdState, msg: msgState, end: endState}, {anm: anmDispatch, std: stdDispatch, msg: msgDispatch, end: endDispatch}];
}

function useLangStateReducer(settings: SavedSettings, lang: Lang): [LangState, React.Dispatch<LangAction>] {
Expand Down
6 changes: 4 additions & 2 deletions js/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ export function settingsPreAppInit() {
fixOldLocalStorageKeys();
}

export type Lang = 'anm' | 'msg' | 'std';
export type Lang = 'anm' | 'msg' | 'std' | 'end';
export type BuiltinNameSet = 'raw' | 'truth';
export function allBuiltins(): BuiltinNameSet[] { return ['raw', 'truth']; }
export function defaultBuiltin(): BuiltinNameSet { return 'truth'; }
export function allLangs(): Lang[] { return ['anm', 'msg', 'std']; }
export function allLangs(): Lang[] { return ['anm', 'msg', 'std', 'end']; }

const truthMapPath = './mapfile';
const truthGamemaps: {[L in Lang]: string} = {
anm: 'any.anmm',
std: 'any.stdm',
msg: 'any.msgm',
end: 'any.endm',
};

/** Form of an ECLMap stored in memory. */
Expand Down Expand Up @@ -58,6 +59,7 @@ export type SavedSettings = {
anm: SavedLangSettings,
std: SavedLangSettings,
msg: SavedLangSettings,
end: SavedLangSettings,
};

export type SavedLangSettings = {
Expand Down
213 changes: 213 additions & 0 deletions js/tables/reference/end.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import {mapAssign} from '~/js/util';
import dedent from '~/js/lib/dedent';

import type {PartialInsData, PartialOpcodeRefData} from '../tables';
import {Game} from '../game';

// ==========================================================================
// ==========================================================================
// =================== INFO TEXT =========================

export function getEndTableText(game: Game) {
const furiTip = `"
After an instruction that contains furigana, the string argument in the NEXT instruction
will contain extra garbage. Basically, after applying the XOR mask to the data,
you will find a copy of the encrypted bytes from the furigana string after
the null terminator. This is likely a bug in ZUN&#39;s compiler.
"`;

let furibug = '';
if ('20' <= game) {
furibug = `:game[20] and onwards additionally have :tip[an unusual quirk]{tip=${furiTip}} near furigana strings.`;
}
return dedent(`
> **Notice:** END files are (de-)compiled by \`trumsg --end\`.
>
> ...or, they *will* be, once I add the necessary core mapfiles, which so far I have... not. In the meanwhile
> you can run trumsg with a mapfile containing END signatures using the \`-m\` flag.

* Most strings are null-terminated and null-padded
up to a multiple of 4 bytes, and are encoded in Shift-JIS.
* For :ref{r=end:text-add} **only**, the string is also then masked the same as MSG: by XOR with an
accelerating bitmask with initial value \`0x77\`, intial velocity \`0x07\`, and constant acceleration \`0x10\`.
Specifically, the first byte is XORed with \`0x77\`, the second byte is XORed with
\`0x7e (= 0x77 + 0x07)\`, the third byte is XORed with \`0x95 (= 0x77 + 0x07 + 0x17)\`,
the fourth byte is XORed with \`0x27 (= 0x77 + 0x07 + 0x17 + 0x27)\`, and so on...

${furibug}
`);
}

// ==========================================================================
// ==========================================================================
// =================== LOOKUP TABLE BY OPCODE =========================

export const refByOpcode = new Map<Game, Map<number, PartialOpcodeRefData>>();

const SHARED_END_OPCODE_MAP = new Map<number, PartialOpcodeRefData>([
[0, {ref: 'end:delete'}],
[3, {ref: 'end:text-add'}],
[4, {ref: 'end:text-clear'}],
[5, {ref: 'end:wait'}],
[6, {ref: 'end:wait-clear'}],
[7, {ref: 'end:anm-source-load'}],
[8, {ref: 'end:anm-set-slot'}],
[9, {ref: 'end:text-color'}],
[10, {ref: 'end:music'}],
[11, {ref: 'end:music-fade'}],
[12, {ref: 'end:switch-to-staff'}],
[13, {ref: 'end:screen-effect-a'}],
[14, {ref: 'end:screen-effect-b'}],
[15, {ref: 'end:anm-set-slot-normal'}],
[16, {ref: 'end:anm-set-slot-hard'}],
[17, {ref: 'end:anm-set-slot-lunatic'}],
]);

for (const game of ['10', '11', '12', '128', '13', '14', '15', '16', '17', '18'] as const) {
refByOpcode.set(game, new Map(SHARED_END_OPCODE_MAP));
}

// ==========================================================================
// ==========================================================================
// ===================== INSTRUCTION DATA =============================

// Lookup table by ref id. (game-independent, map-independent name)
export const byRefId = new Map<string, PartialInsData>();

mapAssign(byRefId, {
'delete': {
sig: '', args: [], md: `Completely kills the script engine.`,
},
'text-add': {
sig: 'm', args: ['text'], md: `
:tipshow[Sets the next line of text.]

**Reminder:** This instruction--and only this instruction--masks its string. (see top of page)

<!-- WHEN TH20 is added, uncomment the following:
Beginning in :game[20], this instruction can also be used to set furigana.
If the first character of the string is \`|\`, then it will set furigana for the next line.
Each line can have at most one furigana annotation.
-->

<!-- :game[18] and :game[20] have 5 lines. -->
* :game[18] has 5 lines.
* :wip[The counts for the other games is pending research.]

After writing the last line, the "next line" index wraps to 0 and a flag is set. The next call to :ref{r=end:text-add}
will clear all lines before writing the first line.

<!-- TH20 and beyond:

The format of furigana includes some offset information.
~~~anm
:ref{r=end:text-add}("|0,11,シーフ");
:ref{r=end:text-add}("盗賊だからなぁ");
~~~
These parameters are \`|xoffset,spacing,\`.
(these are standard parameters to the function that draws lines of text;
for point of reference, on non-furigana lines they are both 0.)
-->

<!-- fix all the commented stuff when updating to TH20 -->
<!-- NEWHU: 185 -->
`,
},
'text-clear': {
sig: '', args: [], wip: 1, md: `
Hides all text.

The "next line" counter does not get reset to 0, making it awkward to use. The games don't appear to be using it.

:wip2[Validate this disuse when END stats are added?]
`,
},
'wait': {
sig: 'S', args: ['max'], md: `
:tipshow[Waits up to some maximum number of frames for the player to press the Shoot key.]

During this time, the script timer will be frozen, so you do not need to increment
the time label by any similar amount.

Negative arguments are treated as 999.
`,
},
'wait-clear': {
sig: 'S', args: ['max'], md: `
:tipshow[Waits up to some maximum number of frames for the player to press the Shoot key.]

During this time, the script timer will be frozen, so you do not need to increment
the time label by any similar amount.

The next call to :ref{r=end:text-add} will clear all lines and begin at index 0.

Unlike :ref{r=end:wait}, negative arguments are NOT treated as 999.
`,
},
'anm-source-load': {
sig: 'Sm', args: ['source_index', 'file'], wip: 1, md: ``,
},
'anm-set-slot': {
sig: 'SSS', args: ['slot', 'source_index', 'script'], wip: 1, md: ``,
},
'text-color': {
sig: 'S', args: ['color'], md: `Euh. Color.`,
},
'music': {
sig: 'm', args: ['file'], md: `
:tipshow[Begins playing music.]

You can play any song in the bgm file. It also unlocks a song in the music player, however,
the song unlocked is not necessarily the one played. (e.g. in :game[18], asking for the
ending music plays the ending music; and anything else gives the staff roll music.)
`,
},
'music-fade': {
sig: '', args: [], md: `
:tipshow[Fades music out at the end of the stage.]

* In :game[18], the fade is hardcoded to last 3 seconds.
* :wip[Durations for other games are pending research.]
`,
},
'switch-to-staff': {
sig: 'm', args: ['end_file'], md: `
:tipshow[Begin the staff roll.]

Staff MSG files are the same format as ending files, so this instruction basically
reinitializes the entire END script parser. (in :game[18] at least)

<!-- WHEN TH20 is added, uncomment:
* (:game[10]&ndash;:game[19]) The string argument is ignored. In :game[18], the instruction is hardcoded to
pick from four staff roll files based on difficulty (though all four files are identical).
* (:game[20]&ndash;) The string argument specifies which staff roll file to load.
-->
<!-- NEWHU: 185 -->

* In :game[18], the string argument is ignored. The instruction is hardcoded to pick from four staff roll
files based on difficulty (though all four files are identical).
* :wip[Behavior in other games is pending research.]
`,
},
'screen-effect-a': {
sig: 'S', args: ['unk'], wip: 2, md: `Does. Something.`,
},
'screen-effect-b': {
sig: 'S', args: ['unk'], wip: 2, md: `Does. Something.`,
},
'anm-set-slot-normal': {
sig: 'SSS', args: ['slot', 'source_index', 'script'], md: `
Identical to :ref{r=end:anm-set-slot}, but only runs on NORMAL difficulty.
`,
},
'anm-set-slot-hard': {
sig: 'SSS', args: ['slot', 'source_index', 'script'], md: `
Identical to :ref{r=end:anm-set-slot}, but only runs on HARD difficulty.
`,
},
'anm-set-slot-lunatic': {
sig: 'SSS', args: ['slot', 'source_index', 'script'], md: `
Identical to :ref{r=end:anm-set-slot}, but only runs on LUNATIC difficulty.
`,
},
});
Loading