@sattori/touhou-replay-parser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/dist/byte-reader.d.ts +48 -0
  4. package/dist/byte-reader.d.ts.map +1 -0
  5. package/dist/byte-reader.js +129 -0
  6. package/dist/byte-reader.js.map +1 -0
  7. package/dist/errors.d.ts +10 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +13 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/game-ids.d.ts +13 -0
  12. package/dist/game-ids.d.ts.map +1 -0
  13. package/dist/game-ids.js +56 -0
  14. package/dist/game-ids.js.map +1 -0
  15. package/dist/games/modern-body.d.ts +16 -0
  16. package/dist/games/modern-body.d.ts.map +1 -0
  17. package/dist/games/modern-body.js +21 -0
  18. package/dist/games/modern-body.js.map +1 -0
  19. package/dist/games/th06.d.ts +8 -0
  20. package/dist/games/th06.d.ts.map +1 -0
  21. package/dist/games/th06.js +85 -0
  22. package/dist/games/th06.js.map +1 -0
  23. package/dist/games/th07.d.ts +12 -0
  24. package/dist/games/th07.d.ts.map +1 -0
  25. package/dist/games/th07.js +138 -0
  26. package/dist/games/th07.js.map +1 -0
  27. package/dist/games/th08.d.ts +6 -0
  28. package/dist/games/th08.d.ts.map +1 -0
  29. package/dist/games/th08.js +118 -0
  30. package/dist/games/th08.js.map +1 -0
  31. package/dist/games/th09.d.ts +10 -0
  32. package/dist/games/th09.d.ts.map +1 -0
  33. package/dist/games/th09.js +110 -0
  34. package/dist/games/th09.js.map +1 -0
  35. package/dist/games/th095.d.ts +7 -0
  36. package/dist/games/th095.d.ts.map +1 -0
  37. package/dist/games/th095.js +39 -0
  38. package/dist/games/th095.js.map +1 -0
  39. package/dist/games/th10.d.ts +4 -0
  40. package/dist/games/th10.d.ts.map +1 -0
  41. package/dist/games/th10.js +50 -0
  42. package/dist/games/th10.js.map +1 -0
  43. package/dist/games/th11.d.ts +4 -0
  44. package/dist/games/th11.d.ts.map +1 -0
  45. package/dist/games/th11.js +49 -0
  46. package/dist/games/th11.js.map +1 -0
  47. package/dist/games/th12.d.ts +4 -0
  48. package/dist/games/th12.d.ts.map +1 -0
  49. package/dist/games/th12.js +58 -0
  50. package/dist/games/th12.js.map +1 -0
  51. package/dist/games/th125.d.ts +6 -0
  52. package/dist/games/th125.d.ts.map +1 -0
  53. package/dist/games/th125.js +39 -0
  54. package/dist/games/th125.js.map +1 -0
  55. package/dist/games/th128.d.ts +9 -0
  56. package/dist/games/th128.d.ts.map +1 -0
  57. package/dist/games/th128.js +79 -0
  58. package/dist/games/th128.js.map +1 -0
  59. package/dist/games/th13-14.d.ts +17 -0
  60. package/dist/games/th13-14.d.ts.map +1 -0
  61. package/dist/games/th13-14.js +110 -0
  62. package/dist/games/th13-14.js.map +1 -0
  63. package/dist/games/th143.d.ts +7 -0
  64. package/dist/games/th143.d.ts.map +1 -0
  65. package/dist/games/th143.js +39 -0
  66. package/dist/games/th143.js.map +1 -0
  67. package/dist/games/th15.d.ts +4 -0
  68. package/dist/games/th15.d.ts.map +1 -0
  69. package/dist/games/th15.js +49 -0
  70. package/dist/games/th15.js.map +1 -0
  71. package/dist/games/th16.d.ts +4 -0
  72. package/dist/games/th16.d.ts.map +1 -0
  73. package/dist/games/th16.js +51 -0
  74. package/dist/games/th16.js.map +1 -0
  75. package/dist/games/th17.d.ts +4 -0
  76. package/dist/games/th17.d.ts.map +1 -0
  77. package/dist/games/th17.js +49 -0
  78. package/dist/games/th17.js.map +1 -0
  79. package/dist/games/th18.d.ts +4 -0
  80. package/dist/games/th18.d.ts.map +1 -0
  81. package/dist/games/th18.js +58 -0
  82. package/dist/games/th18.js.map +1 -0
  83. package/dist/games/th20.d.ts +22 -0
  84. package/dist/games/th20.d.ts.map +1 -0
  85. package/dist/games/th20.js +41 -0
  86. package/dist/games/th20.js.map +1 -0
  87. package/dist/index.d.ts +6 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +70 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/lzss.d.ts +16 -0
  92. package/dist/lzss.d.ts.map +1 -0
  93. package/dist/lzss.js +147 -0
  94. package/dist/lzss.js.map +1 -0
  95. package/dist/types.d.ts +125 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +22 -0
  98. package/dist/types.js.map +1 -0
  99. package/dist/userdata.d.ts +24 -0
  100. package/dist/userdata.d.ts.map +1 -0
  101. package/dist/userdata.js +52 -0
  102. package/dist/userdata.js.map +1 -0
  103. package/package.json +41 -0
@@ -0,0 +1,4 @@
1
+ import { type ParsedReplay } from "../types.js";
2
+ /** t18r (東方虹龍洞, UM) decoder. Ported from Read_t18r in threplay. */
3
+ export declare function parseTh18(original: Uint8Array): ParsedReplay;
4
+ //# sourceMappingURL=th18.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"th18.d.ts","sourceRoot":"","sources":["../../src/games/th18.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4C,KAAK,YAAY,EAAyB,MAAM,aAAa,CAAC;AAIjH,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,UAAU,GAAG,YAAY,CAwD5D"}
@@ -0,0 +1,58 @@
1
+ import { ByteReader } from "../byte-reader.js";
2
+ import { readBufferedUint32LE } from "../lzss.js";
3
+ import { readModernUserdata } from "../userdata.js";
4
+ import { emptySplit, normalizeText, resourceCount } from "../types.js";
5
+ import { REPLAY_GAME_TITLES } from "../game-ids.js";
6
+ import { decodeModernBody } from "./modern-body.js";
7
+ /** t18r (東方虹龍洞, UM) decoder. Ported from Read_t18r in threplay. */
8
+ export function parseTh18(original) {
9
+ const decodedata = decodeModernBody(original, { blockSize: 0x400, base: 0x5c, add: 0xe1 }, { blockSize: 0x100, base: 0x7d, add: 0x3a });
10
+ const splits = [];
11
+ let stageOffset = 0xc8;
12
+ let frameCount = 0;
13
+ const stageCount = Math.min(decodedata[0xa8] ?? 0, 6);
14
+ for (let i = 0; i < stageCount; i++) {
15
+ const split = emptySplit();
16
+ split.stage = decodedata[stageOffset] ?? null;
17
+ split.score = readBufferedUint32LE(decodedata, stageOffset + 0x88) * 10;
18
+ split.power = (readBufferedUint32LE(decodedata, stageOffset + 0xc4) / 100).toFixed(2);
19
+ split.piv = readBufferedUint32LE(decodedata, stageOffset + 0xbc);
20
+ const lives = decodedata[stageOffset + 0xd4] ?? 0;
21
+ const livePieces = decodedata[stageOffset + 0xd8] ?? 0;
22
+ const bombs = decodedata[stageOffset + 0xe4] ?? 0;
23
+ const bombPieces = decodedata[stageOffset + 0xe8] ?? 0;
24
+ const cards = [];
25
+ for (let cardByteOffset = 0x160; cardByteOffset < 1376; cardByteOffset += 4) {
26
+ const cardId = readBufferedUint32LE(decodedata, stageOffset + cardByteOffset);
27
+ if (cardId === 0xffffffff)
28
+ break;
29
+ cards.push(cardId);
30
+ }
31
+ const active = readBufferedUint32LE(decodedata, stageOffset + 2400);
32
+ split.additional = { cards, active };
33
+ split.lives = resourceCount(lives, livePieces, 3);
34
+ split.graze = 0;
35
+ split.bombs = resourceCount(bombs, bombPieces, 3);
36
+ const stageFrameCount = readBufferedUint32LE(decodedata, stageOffset + 0x4);
37
+ split.frameCount = stageFrameCount;
38
+ splits.push(split);
39
+ frameCount += stageFrameCount;
40
+ stageOffset += readBufferedUint32LE(decodedata, stageOffset + 0x8) + 0x126c;
41
+ }
42
+ const userdata = readModernUserdata(new ByteReader(original));
43
+ return {
44
+ game: "th18",
45
+ gameTitle: REPLAY_GAME_TITLES.th18,
46
+ formatVersion: null,
47
+ player: normalizeText(userdata.name),
48
+ date: normalizeText(userdata.date),
49
+ character: normalizeText(userdata.character),
50
+ difficulty: normalizeText(userdata.difficulty),
51
+ stage: normalizeText(userdata.stage),
52
+ score: userdata.score,
53
+ cleared: userdata.stage.includes("Clear"),
54
+ splits,
55
+ frameCount: stageCount > 0 ? frameCount : null,
56
+ };
57
+ }
58
+ //# sourceMappingURL=th18.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"th18.js","sourceRoot":"","sources":["../../src/games/th18.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { type ParsedReplay } from "../types.js";
2
+ /**
3
+ * t20r (東方錦上京, FW) decoder.
4
+ *
5
+ * threplay (raviddog/threplay, the upstream this package was ported from)
6
+ * only covers up to th18; th20 is implemented based on this package's own
7
+ * investigation. The USER section layout (player name, date, character,
8
+ * difficulty, stage, score after JumpToUser(12)) has been confirmed to be
9
+ * identical to th10-th18 by cross-checking real replays
10
+ * (`touhou-recorder/games/th20/replay/*.rpy`) against screenshots.
11
+ *
12
+ * On the other hand, the "per-stage breakdown via XOR decoding + LZSS
13
+ * decompression using the length/dlength at header offset 0x1c/0x20" present
14
+ * in th10-th18 does not appear to carry the same meaning in th20: across all
15
+ * 3 samples on hand, the decompressed size stays constant (256 bytes)
16
+ * regardless of progress (likely because a format change moved that data
17
+ * elsewhere). Since the stage-breakdown structure has not been analyzed yet,
18
+ * `splits` always returns an empty array (name/date/character/difficulty/stage/score
19
+ * can still be obtained).
20
+ */
21
+ export declare function parseTh20(original: Uint8Array): ParsedReplay;
22
+ //# sourceMappingURL=th20.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"th20.d.ts","sourceRoot":"","sources":["../../src/games/th20.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAG/D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,UAAU,GAAG,YAAY,CAiB5D"}
@@ -0,0 +1,41 @@
1
+ import { ByteReader } from "../byte-reader.js";
2
+ import { readModernUserdata } from "../userdata.js";
3
+ import { normalizeText } from "../types.js";
4
+ import { REPLAY_GAME_TITLES } from "../game-ids.js";
5
+ /**
6
+ * t20r (東方錦上京, FW) decoder.
7
+ *
8
+ * threplay (raviddog/threplay, the upstream this package was ported from)
9
+ * only covers up to th18; th20 is implemented based on this package's own
10
+ * investigation. The USER section layout (player name, date, character,
11
+ * difficulty, stage, score after JumpToUser(12)) has been confirmed to be
12
+ * identical to th10-th18 by cross-checking real replays
13
+ * (`touhou-recorder/games/th20/replay/*.rpy`) against screenshots.
14
+ *
15
+ * On the other hand, the "per-stage breakdown via XOR decoding + LZSS
16
+ * decompression using the length/dlength at header offset 0x1c/0x20" present
17
+ * in th10-th18 does not appear to carry the same meaning in th20: across all
18
+ * 3 samples on hand, the decompressed size stays constant (256 bytes)
19
+ * regardless of progress (likely because a format change moved that data
20
+ * elsewhere). Since the stage-breakdown structure has not been analyzed yet,
21
+ * `splits` always returns an empty array (name/date/character/difficulty/stage/score
22
+ * can still be obtained).
23
+ */
24
+ export function parseTh20(original) {
25
+ const userdata = readModernUserdata(new ByteReader(original));
26
+ return {
27
+ game: "th20",
28
+ gameTitle: REPLAY_GAME_TITLES.th20,
29
+ formatVersion: null,
30
+ player: normalizeText(userdata.name),
31
+ date: normalizeText(userdata.date),
32
+ character: normalizeText(userdata.character),
33
+ difficulty: normalizeText(userdata.difficulty),
34
+ stage: normalizeText(userdata.stage),
35
+ score: userdata.score,
36
+ cleared: userdata.stage.includes("Clear"),
37
+ splits: [],
38
+ frameCount: null,
39
+ };
40
+ }
41
+ //# sourceMappingURL=th20.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"th20.js","sourceRoot":"","sources":["../../src/games/th20.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAqB,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CAAC,QAAoB;IAC5C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE9D,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,SAAS,EAAE,kBAAkB,CAAC,IAAI;QAClC,aAAa,EAAE,IAAI;QACnB,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;QACpC,IAAI,EAAE,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;QAClC,SAAS,EAAE,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC;QAC5C,UAAU,EAAE,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC;QAC9C,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC;QACpC,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;QACzC,MAAM,EAAE,EAAE;QACV,UAAU,EAAE,IAAI;KACjB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { ReplayParseResult } from "./types.js";
2
+ export * from "./game-ids.js";
3
+ export * from "./types.js";
4
+ export { ReplayCorruptError } from "./errors.js";
5
+ export declare function parseReplay(data: Uint8Array): ReplayParseResult;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAgB,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAElE,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AA6BjD,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAyB/D"}
package/dist/index.js ADDED
@@ -0,0 +1,70 @@
1
+ import { ReplayCorruptError } from "./errors.js";
2
+ import { parseTh06 } from "./games/th06.js";
3
+ import { parseTh07 } from "./games/th07.js";
4
+ import { parseTh08 } from "./games/th08.js";
5
+ import { parseTh09 } from "./games/th09.js";
6
+ import { parseTh095 } from "./games/th095.js";
7
+ import { parseTh125 } from "./games/th125.js";
8
+ import { parseTh128 } from "./games/th128.js";
9
+ import { parseTh143Family } from "./games/th143.js";
10
+ import { parseTh10 } from "./games/th10.js";
11
+ import { parseTh11 } from "./games/th11.js";
12
+ import { parseTh12 } from "./games/th12.js";
13
+ import { parseTh1314 } from "./games/th13-14.js";
14
+ import { parseTh15 } from "./games/th15.js";
15
+ import { parseTh16 } from "./games/th16.js";
16
+ import { parseTh17 } from "./games/th17.js";
17
+ import { parseTh18 } from "./games/th18.js";
18
+ import { parseTh20 } from "./games/th20.js";
19
+ export * from "./game-ids.js";
20
+ export * from "./types.js";
21
+ export { ReplayCorruptError } from "./errors.js";
22
+ function magicOf(buffer) {
23
+ return Array.from(buffer.subarray(0, 4), (b) => String.fromCharCode(b)).join("");
24
+ }
25
+ const DECODERS = {
26
+ T6RP: parseTh06,
27
+ T7RP: parseTh07,
28
+ T8RP: parseTh08,
29
+ T9RP: parseTh09,
30
+ t95r: parseTh095,
31
+ t125: parseTh125,
32
+ "128r": parseTh128,
33
+ t143: (buffer) => parseTh143Family(buffer, "th143"),
34
+ t156: (buffer) => parseTh143Family(buffer, "th165"),
35
+ t10r: parseTh10,
36
+ t11r: parseTh11,
37
+ t12r: parseTh12,
38
+ t13r: parseTh1314,
39
+ t15r: parseTh15,
40
+ t16r: parseTh16,
41
+ t17r: parseTh17,
42
+ t18r: parseTh18,
43
+ t20r: parseTh20,
44
+ };
45
+ export function parseReplay(data) {
46
+ if (data.length < 4) {
47
+ return { ok: false, error: { code: "too_short", message: "file is shorter than the 4-byte magic header" } };
48
+ }
49
+ const magic = magicOf(data);
50
+ const decode = DECODERS[magic];
51
+ if (!decode) {
52
+ return {
53
+ ok: false,
54
+ error: { code: "unknown_magic", message: `unrecognized replay magic: ${JSON.stringify(magic)}` },
55
+ };
56
+ }
57
+ try {
58
+ const replay = decode(data);
59
+ return { ok: true, replay };
60
+ }
61
+ catch (error) {
62
+ if (error instanceof ReplayCorruptError) {
63
+ return { ok: false, error: { code: "corrupt", message: error.message } };
64
+ }
65
+ // Fail safe on unknown exceptions too — never throw.
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ return { ok: false, error: { code: "corrupt", message: `unexpected error while parsing: ${message}` } };
68
+ }
69
+ }
70
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/lzss.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export declare function assertSaneAllocSize(n: number, what: string): void;
2
+ /**
3
+ * An LZSS-family decompressor (13-bit dictionary / 4-bit run length) ported
4
+ * from decompress() in threp (https://github.com/Fluorohydride/threp/blob/master/common.cpp).
5
+ * `length` is the input (compressed) byte count, `outLength` is the expected decompressed byte count.
6
+ */
7
+ export declare function decompress(buffer: Uint8Array, length: number, outLength: number): Uint8Array;
8
+ /**
9
+ * Ported from threp's decode() (block-wise XOR deobfuscation).
10
+ * Rewrites the first `length` bytes of `buffer` in place.
11
+ */
12
+ export declare function xorBlockDecode(buffer: Uint8Array, length: number, blockSizeInit: number, baseInit: number, add: number): void;
13
+ /** Decodes the simple additive-key scheme used by the older format (th06-09). */
14
+ export declare function additiveKeyDecode(buffer: Uint8Array, start: number, initialKey: number, increment: number): void;
15
+ export declare function readBufferedUint32LE(buffer: Uint8Array, offset: number): number;
16
+ //# sourceMappingURL=lzss.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lzss.d.ts","sourceRoot":"","sources":["../src/lzss.ts"],"names":[],"mappings":"AASA,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAIjE;AAoCD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,CA0C5F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAqC7H;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAMhH;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAO/E"}
package/dist/lzss.js ADDED
@@ -0,0 +1,147 @@
1
+ import { ReplayCorruptError } from "./errors.js";
2
+ /**
3
+ * Upper bound (64MiB) used to sanity-check decompressed sizes.
4
+ * The header's length/dlength fields are untrusted external input, so
5
+ * without this guard an invalid huge value would attempt an unbounded allocation.
6
+ */
7
+ const MAX_ALLOC_BYTES = 64 * 1024 * 1024;
8
+ export function assertSaneAllocSize(n, what) {
9
+ if (!Number.isFinite(n) || n < 0 || n > MAX_ALLOC_BYTES) {
10
+ throw new ReplayCorruptError(`implausible ${what}: ${n}`);
11
+ }
12
+ }
13
+ function getBit(buffer, length, cursor, bitLength) {
14
+ // https://github.com/Fluorohydride/thprac/blob/master/common.cpp (ported from get_bit in common.cpp)
15
+ let result = 0;
16
+ for (let i = 0; i < bitLength; i++) {
17
+ result <<= 1;
18
+ if (cursor.pointer >= length) {
19
+ // The original implementation can read past the buffer boundary, but this
20
+ // is harmless because the caller immediately checks `pointer >= length`
21
+ // and stops. Here we fail safe by zero-filling.
22
+ cursor.filter >>= 1;
23
+ if (cursor.filter === 0) {
24
+ cursor.pointer++;
25
+ cursor.filter = 0x80;
26
+ }
27
+ continue;
28
+ }
29
+ const current = buffer[cursor.pointer];
30
+ if ((current & cursor.filter) !== 0) {
31
+ result |= 1;
32
+ }
33
+ cursor.filter >>= 1;
34
+ if (cursor.filter === 0) {
35
+ cursor.pointer++;
36
+ cursor.filter = 0x80;
37
+ }
38
+ }
39
+ return result >>> 0;
40
+ }
41
+ /**
42
+ * An LZSS-family decompressor (13-bit dictionary / 4-bit run length) ported
43
+ * from decompress() in threp (https://github.com/Fluorohydride/threp/blob/master/common.cpp).
44
+ * `length` is the input (compressed) byte count, `outLength` is the expected decompressed byte count.
45
+ */
46
+ export function decompress(buffer, length, outLength) {
47
+ assertSaneAllocSize(outLength, "decompressed length");
48
+ // length is an untrusted value read from the header. A value exceeding the
49
+ // buffer's actual size indicates corrupt data, and using it as-is would let
50
+ // the loop run for an unreasonably long time relative to the actual data
51
+ // size (a potential DoS), so we clamp it here to a physically possible value.
52
+ const boundedLength = Math.min(length, buffer.length);
53
+ const decoded = new Uint8Array(outLength);
54
+ const dict = new Uint8Array(0x2010);
55
+ const cursor = { pointer: 0, filter: 0x80 };
56
+ let dest = 0;
57
+ while (cursor.pointer < boundedLength && dest < outLength) {
58
+ const flag = getBit(buffer, boundedLength, cursor, 1);
59
+ if (cursor.pointer >= boundedLength)
60
+ break;
61
+ if (flag !== 0) {
62
+ const byte = getBit(buffer, boundedLength, cursor, 8);
63
+ if (cursor.pointer >= boundedLength)
64
+ break;
65
+ decoded[dest] = byte;
66
+ dict[dest & 0x1fff] = byte;
67
+ dest++;
68
+ }
69
+ else {
70
+ const indexBits = getBit(buffer, boundedLength, cursor, 13);
71
+ if (cursor.pointer >= boundedLength)
72
+ break;
73
+ const index = indexBits - 1;
74
+ let runLength = getBit(buffer, boundedLength, cursor, 4);
75
+ if (cursor.pointer >= boundedLength)
76
+ break;
77
+ runLength += 3;
78
+ for (let i = 0; i < runLength && dest < outLength; i++) {
79
+ // index can be negative or out of range for invalid input, but
80
+ // out-of-range TypedArray read/write does not throw and is
81
+ // coerced to undefined -> 0, so this is safe.
82
+ const value = dict[(index + i) & 0x1fff] ?? 0;
83
+ dict[dest & 0x1fff] = value;
84
+ decoded[dest] = value;
85
+ dest++;
86
+ }
87
+ }
88
+ }
89
+ return decoded;
90
+ }
91
+ /**
92
+ * Ported from threp's decode() (block-wise XOR deobfuscation).
93
+ * Rewrites the first `length` bytes of `buffer` in place.
94
+ */
95
+ export function xorBlockDecode(buffer, length, blockSizeInit, baseInit, add) {
96
+ assertSaneAllocSize(length, "xorBlockDecode length");
97
+ if (length > buffer.length) {
98
+ throw new ReplayCorruptError(`xorBlockDecode: length ${length} exceeds buffer size ${buffer.length}`);
99
+ }
100
+ const tbuf = buffer.slice(0, length);
101
+ let blockSize = blockSizeInit;
102
+ let p = 0;
103
+ let left = length;
104
+ if (left % blockSize < Math.floor(blockSize / 4)) {
105
+ left -= left % blockSize;
106
+ }
107
+ left -= length & 1;
108
+ let base = baseInit & 0xff;
109
+ while (left !== 0) {
110
+ if (left < blockSize)
111
+ blockSize = left;
112
+ let tp1 = p + blockSize - 1;
113
+ let tp2 = p + blockSize - 2;
114
+ let half = (blockSize + (blockSize & 1)) >> 1;
115
+ for (let i = 0; i < half; i++, p++) {
116
+ if (tp1 >= 0 && tp1 < buffer.length && p < tbuf.length) {
117
+ buffer[tp1] = (tbuf[p] ^ base) & 0xff;
118
+ }
119
+ base = (base + add) & 0xff;
120
+ tp1 -= 2;
121
+ }
122
+ half = blockSize >> 1;
123
+ for (let i = 0; i < half; i++, p++) {
124
+ if (tp2 >= 0 && tp2 < buffer.length && p < tbuf.length) {
125
+ buffer[tp2] = (tbuf[p] ^ base) & 0xff;
126
+ }
127
+ base = (base + add) & 0xff;
128
+ tp2 -= 2;
129
+ }
130
+ left -= blockSize;
131
+ }
132
+ }
133
+ /** Decodes the simple additive-key scheme used by the older format (th06-09). */
134
+ export function additiveKeyDecode(buffer, start, initialKey, increment) {
135
+ let key = initialKey & 0xff;
136
+ for (let i = start; i < buffer.length; i++) {
137
+ buffer[i] = (buffer[i] - key) & 0xff;
138
+ key = (key + increment) & 0xff;
139
+ }
140
+ }
141
+ export function readBufferedUint32LE(buffer, offset) {
142
+ if (offset < 0 || offset + 4 > buffer.length) {
143
+ throw new ReplayCorruptError(`readBufferedUint32LE out of range at ${offset} (length ${buffer.length})`);
144
+ }
145
+ return ((buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24)) >>> 0);
146
+ }
147
+ //# sourceMappingURL=lzss.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lzss.js","sourceRoot":"","sources":["../src/lzss.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ import type { ReplayGameId } from "./game-ids.js";
2
+ /**
3
+ * A resource count made up of a "whole unit count" plus "fragments toward the
4
+ * next unit," as used for lives, bombs, etc. `pieces`/`maxPieces` are null for
5
+ * games without a fragment system, or when the data cannot be obtained for
6
+ * that game.
7
+ *
8
+ * Exception: th128 (妖精大戦争, GFW) uses this field as a percentage gauge —
9
+ * `count` holds the percentage (0-100+), `maxPieces` is always 100, and
10
+ * `pieces` is always null (see the comments in that file).
11
+ */
12
+ export interface ReplayResourceCount {
13
+ /** The whole unit count (or a percentage, for th128 only). */
14
+ count: number;
15
+ /** Number of fragments collected toward the next unit. */
16
+ pieces: number | null;
17
+ /** The fragment maximum (denominator). Null if it cannot be determined. */
18
+ maxPieces: number | null;
19
+ }
20
+ /**
21
+ * A per-stage record. Games track different fields, so fields not tracked by
22
+ * a given game are null.
23
+ */
24
+ export interface ReplayStageSplit {
25
+ /**
26
+ * Stage number (null if it cannot be determined). Most games record this
27
+ * as a snapshot taken "at the start of the stage," so fields like `score`
28
+ * effectively reflect the value "at the end of the previous stage"
29
+ * (this matches the value shown on the "Stage N" line of the original
30
+ * game's replay selection screen).
31
+ */
32
+ stage: number | null;
33
+ /** Score at the time of this snapshot. */
34
+ score: number | null;
35
+ /** Power (kept as a string since notation differs by game, e.g. "1.00", "128"). */
36
+ power: string | null;
37
+ /** Game-specific score metric such as PIV (Point of Item Value). */
38
+ piv: number | null;
39
+ /** Lives. */
40
+ lives: ReplayResourceCount | null;
41
+ /** Bomb count. */
42
+ bombs: ReplayResourceCount | null;
43
+ /** Graze count. */
44
+ graze: number | null;
45
+ /**
46
+ * Game-specific extra info such as UFO color, trance, season, etc. Key
47
+ * names and value shapes differ by game (see the comments in each game's
48
+ * decoder implementation). Null for games without such data.
49
+ */
50
+ additional: Record<string, number | string | (number | string)[]> | null;
51
+ /**
52
+ * Number of in-game frames played during this stage/segment (i.e. from
53
+ * this checkpoint up to the next one, or to the end of the replay for the
54
+ * last split). See `ParsedReplay.frameCount` for the fixed-60fps
55
+ * conversion to seconds and the games this is currently populated for.
56
+ * Null for games where this package does not (yet) know how to locate the
57
+ * per-frame input log.
58
+ */
59
+ frameCount: number | null;
60
+ }
61
+ /**
62
+ * The full set of information extractable from a `.rpy` file. `ReplayInfo` in
63
+ * `packages/shared` corresponds to a subset of this type (only the fields
64
+ * needed for Sattori's recording metadata display).
65
+ */
66
+ export interface ParsedReplay {
67
+ game: ReplayGameId;
68
+ gameTitle: string;
69
+ /**
70
+ * The raw format/version byte read from the header (its meaning and offset
71
+ * differ by game). Can be used as a clue to identify replays recorded with
72
+ * an incompatible version of the original game (used by Sattori for #16
73
+ * detection). Null for games where this cannot be determined.
74
+ */
75
+ formatVersion: number | null;
76
+ player: string | null;
77
+ /** Recording date/time, kept verbatim as it appears in the source data (e.g. "25/12/31"). */
78
+ date: string | null;
79
+ character: string | null;
80
+ difficulty: string | null;
81
+ /** The reached/recorded stage or scene notation (e.g. "Stage 6", "Extra"). */
82
+ stage: string | null;
83
+ score: number | null;
84
+ /**
85
+ * True/false only when a full clear (equivalent to "Player Wins") could be
86
+ * detected. Null for games/replay types with no way to determine this.
87
+ */
88
+ cleared: boolean | null;
89
+ /** Per-stage records (empty array for games where this cannot be determined). */
90
+ splits: ReplayStageSplit[];
91
+ /**
92
+ * Total number of in-game frames the replay plays back (all recorded
93
+ * stages/segments summed). The main-series games run gameplay logic at a
94
+ * fixed 60 frames/sec, so dividing by 60 gives the playback duration in
95
+ * seconds. This is the number of frames the game itself will replay, and
96
+ * does not include any recording-pipeline overhead (menu automation,
97
+ * end-of-replay detection lag, etc.) on top of that.
98
+ * Null for games/replay types where this package does not (yet) know how
99
+ * to locate the per-frame input log.
100
+ */
101
+ frameCount: number | null;
102
+ }
103
+ export type ReplayParseErrorCode =
104
+ /** The file is too short to even read the magic bytes. */
105
+ "too_short"
106
+ /** The first 4 bytes don't match any known Touhou replay magic. */
107
+ | "unknown_magic"
108
+ /** The magic is a known format, but the data that follows is invalid and cannot be safely parsed further. */
109
+ | "corrupt";
110
+ export interface ReplayParseError {
111
+ code: ReplayParseErrorCode;
112
+ message: string;
113
+ }
114
+ export type ReplayParseResult = {
115
+ ok: true;
116
+ replay: ParsedReplay;
117
+ } | {
118
+ ok: false;
119
+ error: ReplayParseError;
120
+ };
121
+ /** Strips padding whitespace from fixed-length fields. An empty string is treated as null. */
122
+ export declare function normalizeText(value: string): string | null;
123
+ export declare function emptySplit(): ReplayStageSplit;
124
+ export declare function resourceCount(count: number, pieces?: number | null, maxPieces?: number | null): ReplayResourceCount;
125
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,mFAAmF;IACnF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,oEAAoE;IACpE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,aAAa;IACb,KAAK,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAClC,kBAAkB;IAClB,KAAK,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAClC,mBAAmB;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IACzE;;;;;;;OAOG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,6FAA6F;IAC7F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,8EAA8E;IAC9E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;OAGG;IACH,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,iFAAiF;IACjF,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B;;;;;;;;;OASG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,MAAM,oBAAoB;AAC9B,0DAA0D;AACxD,WAAW;AACb,mEAAmE;GACjE,eAAe;AACjB,6GAA6G;GAC3G,SAAS,CAAC;AAEd,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,oBAAoB,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAE5G,8FAA8F;AAC9F,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG1D;AAED,wBAAgB,UAAU,IAAI,gBAAgB,CAY7C;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,GAAG,IAAW,EAAE,SAAS,GAAE,MAAM,GAAG,IAAW,GAAG,mBAAmB,CAE/H"}
package/dist/types.js ADDED
@@ -0,0 +1,22 @@
1
+ /** Strips padding whitespace from fixed-length fields. An empty string is treated as null. */
2
+ export function normalizeText(value) {
3
+ const trimmed = value.trim();
4
+ return trimmed.length > 0 ? trimmed : null;
5
+ }
6
+ export function emptySplit() {
7
+ return {
8
+ stage: null,
9
+ score: null,
10
+ power: null,
11
+ piv: null,
12
+ lives: null,
13
+ bombs: null,
14
+ graze: null,
15
+ additional: null,
16
+ frameCount: null,
17
+ };
18
+ }
19
+ export function resourceCount(count, pieces = null, maxPieces = null) {
20
+ return { count, pieces, maxPieces };
21
+ }
22
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAyHA,8FAA8F;AAC9F,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI;QACX,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,IAAI;QAChB,UAAU,EAAE,IAAI;KACjB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,MAAM,GAAkB,IAAI,EAAE,SAAS,GAAkB,IAAI;IACxG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AACtC,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { ByteReader } from "./byte-reader.js";
2
+ /**
3
+ * Equivalent to JumpToUser in threplay. Reads the 4-byte offset value at
4
+ * `pointerFieldOffset`, confirms a "USER" marker exists at that position, and
5
+ * then advances the cursor just past it.
6
+ */
7
+ export declare function jumpToUser(reader: ByteReader, pointerFieldOffset: number): void;
8
+ /** Equivalent to `long.TryParse(text + "0", ...)`. Restores the omitted trailing digit before converting to a number. */
9
+ export declare function parseScoreWithTrailingZero(text: string): number | null;
10
+ export declare function parseIntStrict(text: string): number | null;
11
+ export interface CommonUserdataFields {
12
+ name: string;
13
+ date: string;
14
+ character: string;
15
+ difficulty: string;
16
+ stage: string;
17
+ score: number | null;
18
+ }
19
+ /**
20
+ * The USER section layout shared by th10 (東方風神録, MoF) through
21
+ * th18 (東方虹龍洞, UM) (ported from threplay's `Read_Userdata`).
22
+ */
23
+ export declare function readModernUserdata(reader: ByteReader): CommonUserdataFields;
24
+ //# sourceMappingURL=userdata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userdata.d.ts","sourceRoot":"","sources":["../src/userdata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAK9C;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAQ/E;AAED,yHAAyH;AACzH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK1D;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG,oBAAoB,CAkB3E"}