@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.
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/byte-reader.d.ts +48 -0
- package/dist/byte-reader.d.ts.map +1 -0
- package/dist/byte-reader.js +129 -0
- package/dist/byte-reader.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/game-ids.d.ts +13 -0
- package/dist/game-ids.d.ts.map +1 -0
- package/dist/game-ids.js +56 -0
- package/dist/game-ids.js.map +1 -0
- package/dist/games/modern-body.d.ts +16 -0
- package/dist/games/modern-body.d.ts.map +1 -0
- package/dist/games/modern-body.js +21 -0
- package/dist/games/modern-body.js.map +1 -0
- package/dist/games/th06.d.ts +8 -0
- package/dist/games/th06.d.ts.map +1 -0
- package/dist/games/th06.js +85 -0
- package/dist/games/th06.js.map +1 -0
- package/dist/games/th07.d.ts +12 -0
- package/dist/games/th07.d.ts.map +1 -0
- package/dist/games/th07.js +138 -0
- package/dist/games/th07.js.map +1 -0
- package/dist/games/th08.d.ts +6 -0
- package/dist/games/th08.d.ts.map +1 -0
- package/dist/games/th08.js +118 -0
- package/dist/games/th08.js.map +1 -0
- package/dist/games/th09.d.ts +10 -0
- package/dist/games/th09.d.ts.map +1 -0
- package/dist/games/th09.js +110 -0
- package/dist/games/th09.js.map +1 -0
- package/dist/games/th095.d.ts +7 -0
- package/dist/games/th095.d.ts.map +1 -0
- package/dist/games/th095.js +39 -0
- package/dist/games/th095.js.map +1 -0
- package/dist/games/th10.d.ts +4 -0
- package/dist/games/th10.d.ts.map +1 -0
- package/dist/games/th10.js +50 -0
- package/dist/games/th10.js.map +1 -0
- package/dist/games/th11.d.ts +4 -0
- package/dist/games/th11.d.ts.map +1 -0
- package/dist/games/th11.js +49 -0
- package/dist/games/th11.js.map +1 -0
- package/dist/games/th12.d.ts +4 -0
- package/dist/games/th12.d.ts.map +1 -0
- package/dist/games/th12.js +58 -0
- package/dist/games/th12.js.map +1 -0
- package/dist/games/th125.d.ts +6 -0
- package/dist/games/th125.d.ts.map +1 -0
- package/dist/games/th125.js +39 -0
- package/dist/games/th125.js.map +1 -0
- package/dist/games/th128.d.ts +9 -0
- package/dist/games/th128.d.ts.map +1 -0
- package/dist/games/th128.js +79 -0
- package/dist/games/th128.js.map +1 -0
- package/dist/games/th13-14.d.ts +17 -0
- package/dist/games/th13-14.d.ts.map +1 -0
- package/dist/games/th13-14.js +110 -0
- package/dist/games/th13-14.js.map +1 -0
- package/dist/games/th143.d.ts +7 -0
- package/dist/games/th143.d.ts.map +1 -0
- package/dist/games/th143.js +39 -0
- package/dist/games/th143.js.map +1 -0
- package/dist/games/th15.d.ts +4 -0
- package/dist/games/th15.d.ts.map +1 -0
- package/dist/games/th15.js +49 -0
- package/dist/games/th15.js.map +1 -0
- package/dist/games/th16.d.ts +4 -0
- package/dist/games/th16.d.ts.map +1 -0
- package/dist/games/th16.js +51 -0
- package/dist/games/th16.js.map +1 -0
- package/dist/games/th17.d.ts +4 -0
- package/dist/games/th17.d.ts.map +1 -0
- package/dist/games/th17.js +49 -0
- package/dist/games/th17.js.map +1 -0
- package/dist/games/th18.d.ts +4 -0
- package/dist/games/th18.d.ts.map +1 -0
- package/dist/games/th18.js +58 -0
- package/dist/games/th18.js.map +1 -0
- package/dist/games/th20.d.ts +22 -0
- package/dist/games/th20.d.ts.map +1 -0
- package/dist/games/th20.js +41 -0
- package/dist/games/th20.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/lzss.d.ts +16 -0
- package/dist/lzss.d.ts.map +1 -0
- package/dist/lzss.js +147 -0
- package/dist/lzss.js.map +1 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/dist/userdata.d.ts +24 -0
- package/dist/userdata.d.ts.map +1 -0
- package/dist/userdata.js +52 -0
- package/dist/userdata.js.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Koki Takahashi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# @sattori/touhou-replay-parser
|
|
2
|
+
|
|
3
|
+
A zero-dependency TypeScript library for decoding Touhou Project main-series
|
|
4
|
+
replay files (`.rpy`). Written for
|
|
5
|
+
[Sattori](https://github.com/hakatashi/sattori) (a Touhou replay recording web
|
|
6
|
+
service), but designed with no dependency on Sattori-specific types, so it can
|
|
7
|
+
be used standalone.
|
|
8
|
+
|
|
9
|
+
Based on a TypeScript port of `ReplayDecoder.cs` (the C# implementation) from
|
|
10
|
+
[raviddog/threplay](https://github.com/raviddog/threplay), with additions for
|
|
11
|
+
correct decoding of Shift_JIS player names, safe error handling for corrupted
|
|
12
|
+
files, and support for titles that threplay did not cover.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @sattori/touhou-replay-parser
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node.js >= 14 (or a modern browser). Player names and dates are
|
|
21
|
+
decoded as Shift_JIS via the global `TextDecoder`, which needs a full-ICU
|
|
22
|
+
build — the default for official Node.js binaries since Node.js 13. On a
|
|
23
|
+
runtime without Shift_JIS support, decoding silently falls back to Latin1
|
|
24
|
+
(mojibake for Japanese text, not an error) rather than failing outright.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { parseReplay } from "@sattori/touhou-replay-parser";
|
|
30
|
+
import { readFile } from "node:fs/promises";
|
|
31
|
+
|
|
32
|
+
const data = new Uint8Array(await readFile("th7_01.rpy"));
|
|
33
|
+
const result = parseReplay(data);
|
|
34
|
+
|
|
35
|
+
if (result.ok) {
|
|
36
|
+
const { game, player, character, difficulty, score, cleared, splits } = result.replay;
|
|
37
|
+
console.log(`${game}: ${player} / ${character} / ${difficulty} / ${score}`);
|
|
38
|
+
} else {
|
|
39
|
+
// parseReplay never throws. Invalid, unsupported, or corrupted files are
|
|
40
|
+
// returned as a discriminated error code instead.
|
|
41
|
+
console.error(result.error.code, result.error.message);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`parseReplay` never throws. Internally detected corruption is caught as a
|
|
46
|
+
`ReplayCorruptError` and converted to `{ ok: false, error: { code: "corrupt", ... } }`.
|
|
47
|
+
|
|
48
|
+
## Supported titles
|
|
49
|
+
|
|
50
|
+
Titles are identified by the 4-byte magic at the start of the file. th13
|
|
51
|
+
(東方神霊廟, TD) and th14 (東方輝針城, DDC) share the same magic `t13r`, so
|
|
52
|
+
they are distinguished by a version byte in the header.
|
|
53
|
+
|
|
54
|
+
| Game ID | Title | Verification status |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `th06` | 東方紅魔郷 (EoSD) | Verified with checked-in replays + in-game screenshots in `test-fixtures/` |
|
|
57
|
+
| `th07` | 東方妖々夢 (PCB) | Same as above |
|
|
58
|
+
| `th08` | 東方永夜抄 (IN) | Same as above (includes Shift_JIS character names) |
|
|
59
|
+
| `th09` | 東方花映塚 (PoFV) | Verified with real replays (samples obtained from [Silent Selene](https://www.silentselene.net/)) |
|
|
60
|
+
| `th095` | 東方文花帖 (StB) | Same as above |
|
|
61
|
+
| `th10` | 東方風神録 (MoF) | Verified with real replays + screenshots/samples |
|
|
62
|
+
| `th11` | 東方地霊殿 (SA) | Verified with `test-fixtures/` + screenshots |
|
|
63
|
+
| `th12` | 東方星蓮船 (UFO) | Verified with Silent Selene samples |
|
|
64
|
+
| `th125` | ダブルスポイラー (DS) | Verified with checked-in replays in `test-fixtures/` |
|
|
65
|
+
| `th128` | 妖精大戦争 (GFW) | Verified with Silent Selene samples |
|
|
66
|
+
| `th13` | 東方神霊廟 (TD) | Verified with `test-fixtures/` + screenshots |
|
|
67
|
+
| `th14` | 東方輝針城 (DDC) | Same as above |
|
|
68
|
+
| `th143` | 弾幕アマノジャク (ISC) | Verified with checked-in replays in `test-fixtures/` |
|
|
69
|
+
| `th15` | 東方紺珠伝 (LoLK) | Verified with `test-fixtures/` + screenshots |
|
|
70
|
+
| `th16` | 東方天空璋 (HSiFS) | Verified with Silent Selene samples |
|
|
71
|
+
| `th165` | 秘封ナイトメアダイアリー (VD) | **Unverified** (ported from threplay only; no test data obtained yet) |
|
|
72
|
+
| `th17` | 東方鬼形獣 (WBaWC) | Verified with Silent Selene samples |
|
|
73
|
+
| `th18` | 東方虹龍洞 (UM) | Same as above |
|
|
74
|
+
| `th20` | 東方錦上京 (FW) | Player name/date/character/difficulty/stage/score verified with `test-fixtures/` + screenshots.<br>**Per-stage breakdown (splits) is not supported** (see below) |
|
|
75
|
+
|
|
76
|
+
th19 (東方獣王園, UDoALG) is excluded because the game itself has no
|
|
77
|
+
replay-saving feature.
|
|
78
|
+
|
|
79
|
+
### Notes on th20 (東方錦上京, FW)
|
|
80
|
+
|
|
81
|
+
threplay only supports up to th18; th20 is implemented based on this
|
|
82
|
+
package's own investigation. The USER section (player name, date, character,
|
|
83
|
+
difficulty, stage, score) has been confirmed to use the same layout as
|
|
84
|
+
th10-th18, but the "per-stage breakdown via header XOR decoding + LZSS
|
|
85
|
+
decompression" present in th10-th18 appears, on the samples at hand, to
|
|
86
|
+
always decompress to a constant size regardless of progress — suggesting the
|
|
87
|
+
format has likely changed. Since this has not been analyzed, `splits` always
|
|
88
|
+
returns an empty array.
|
|
89
|
+
|
|
90
|
+
## Output data
|
|
91
|
+
|
|
92
|
+
`ParsedReplay` (`result.replay` when `result.ok === true`) carries richer
|
|
93
|
+
information than `ReplayInfo`, the type used by Sattori itself (player name,
|
|
94
|
+
date, character, difficulty, stage, score, clear status). In particular,
|
|
95
|
+
`splits` (a per-stage breakdown of score, power, lives, bombs, graze, etc.)
|
|
96
|
+
and `formatVersion` (the raw version/format byte embedded in the header,
|
|
97
|
+
whose meaning differs per game and which this package does not attempt to
|
|
98
|
+
interpret) are not part of `ReplayInfo`.
|
|
99
|
+
|
|
100
|
+
Conversion to `ReplayInfo` for Sattori itself is handled by `fromParsedReplay()`
|
|
101
|
+
in `packages/shared` (this package deliberately does not include that
|
|
102
|
+
conversion logic, so as to avoid depending on Sattori-specific types).
|
|
103
|
+
|
|
104
|
+
`splits[].lives` / `splits[].bombs` are not strings but a structured
|
|
105
|
+
`ReplayResourceCount` type (`{ count, pieces, maxPieces }`). For games with a
|
|
106
|
+
fragment system (fragments toward the next unit), `pieces`/`maxPieces` are
|
|
107
|
+
populated; for games without one, they are `null` (th128 is the sole
|
|
108
|
+
exception, where `count` holds a percentage and `maxPieces` is always 100 —
|
|
109
|
+
see the comments in `src/games/th128.ts` for details). Likewise,
|
|
110
|
+
`splits[].additional` returns game-specific extra info (UFO color, trance,
|
|
111
|
+
season, spell cards, etc.) as an object with typed properties rather than
|
|
112
|
+
strings (e.g. `{ ufoColors: ["Red", "None", "None"] }`).
|
|
113
|
+
|
|
114
|
+
### `frameCount`
|
|
115
|
+
|
|
116
|
+
`frameCount` is the total number of in-game frames the replay plays back.
|
|
117
|
+
The main-series games run gameplay logic at a fixed 60 frames/sec, so
|
|
118
|
+
`frameCount / 60` gives the playback duration in seconds — useful for
|
|
119
|
+
estimating recording time before actually running the replay. It does not
|
|
120
|
+
include any recording-pipeline overhead (menu automation, end-of-replay
|
|
121
|
+
detection lag, etc.) layered on top by a consumer such as Sattori's worker.
|
|
122
|
+
|
|
123
|
+
Currently populated for:
|
|
124
|
+
|
|
125
|
+
- **th07**: derived by reverse-engineering the per-checkpoint input log
|
|
126
|
+
layout (not documented by threplay/threp, which don't parse this data at
|
|
127
|
+
all). Cross-validated against real recorded durations of a checked-in
|
|
128
|
+
fixture (`touhou-recorder` PoC reports: `th7_07.rpy` recorded at ~840-852s
|
|
129
|
+
end-to-end; the computed frame count lands within that range) — see the
|
|
130
|
+
comments on `STAGE_CHECKPOINT_HEADER_SIZE` in `src/games/th07.ts`.
|
|
131
|
+
- **th10-th18** (all titles sharing the `decodeModernBody` pipeline: th10,
|
|
132
|
+
th11, th12, th13, th14, th15, th16, th17, th18): each stage's decompressed
|
|
133
|
+
header carries an explicit frame-count field, confirmed against
|
|
134
|
+
`Fluorohydride/threp`'s own C++ implementation (which reads this field to
|
|
135
|
+
reconstruct input logs for its own purposes) and independently against
|
|
136
|
+
`yiyuezhuo/touhou-replay-decoder`.
|
|
137
|
+
|
|
138
|
+
`null` for every other supported title (th06, th08, th09, th095, th125,
|
|
139
|
+
th128, th143/th165, th20) — the per-frame input log location for those has
|
|
140
|
+
not been reverse-engineered yet.
|
|
141
|
+
|
|
142
|
+
`splits[].frameCount` breaks the same total down per stage/segment (frames
|
|
143
|
+
played from that checkpoint up to the next one, or to the end of the replay
|
|
144
|
+
for the last split) for the same set of titles; `ParsedReplay.frameCount`
|
|
145
|
+
is simply the sum of every `splits[].frameCount`. It is `null` per-split
|
|
146
|
+
wherever the top-level `frameCount` is also `null`.
|
|
147
|
+
|
|
148
|
+
## Credits
|
|
149
|
+
|
|
150
|
+
Most of the decoding logic was independently written from scratch in
|
|
151
|
+
TypeScript, based on `ReplayDecoder.cs` from
|
|
152
|
+
[raviddog/threplay](https://github.com/raviddog/threplay). The core LZSS
|
|
153
|
+
decompression and XOR block decoding algorithms originate from `common.cpp`
|
|
154
|
+
in [Fluorohydride/threp](https://github.com/Fluorohydride/threp), which that
|
|
155
|
+
repository references.
|
|
156
|
+
|
|
157
|
+
Neither repository carries an explicit OSS license
|
|
158
|
+
(threplay's `LICENCES.txt` only lists licenses for third-party dependencies
|
|
159
|
+
such as UI components, not for `ReplayDecoder.cs` itself). This package is
|
|
160
|
+
published under the MIT license as an independent implementation built from
|
|
161
|
+
factual information (byte offsets, XOR keys, etc.), but please make your own
|
|
162
|
+
judgment call about usage with this background in mind.
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT (see [LICENSE](./LICENSE); also see the "Credits" section above for background)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A low-level cursor for reading through .rpy binary data.
|
|
3
|
+
* Out-of-range access does not throw a generic exception but a
|
|
4
|
+
* ReplayCorruptError (caught at the parseReplay() boundary and
|
|
5
|
+
* converted to a discriminated error value).
|
|
6
|
+
*/
|
|
7
|
+
export declare class ByteReader {
|
|
8
|
+
private readonly bytes;
|
|
9
|
+
private readonly view;
|
|
10
|
+
pos: number;
|
|
11
|
+
constructor(data: Uint8Array);
|
|
12
|
+
get length(): number;
|
|
13
|
+
private ensure;
|
|
14
|
+
readUint8(): number;
|
|
15
|
+
readUint16LE(): number;
|
|
16
|
+
readUint32LE(): number;
|
|
17
|
+
readFloat32LE(): number;
|
|
18
|
+
readBytes(n: number): Uint8Array;
|
|
19
|
+
/** Jumps to an absolute position. */
|
|
20
|
+
seek(offset: number): void;
|
|
21
|
+
/** Moves relative to the current position. */
|
|
22
|
+
skip(delta: number): void;
|
|
23
|
+
slice(start: number, end: number): Uint8Array;
|
|
24
|
+
remaining(): Uint8Array;
|
|
25
|
+
all(): Uint8Array;
|
|
26
|
+
/**
|
|
27
|
+
* Equivalent to ReadStringANSI in raviddog/threplay. Reads until a
|
|
28
|
+
* CR(0x0D)+LF(0x0A) sequence is found as the string terminator, leaving the
|
|
29
|
+
* cursor just before the terminator (right before the 0x0A), so that
|
|
30
|
+
* subsequent fields' `skip()` offsets match the original implementation
|
|
31
|
+
* (this behavior is faithfully reproduced byte-for-byte).
|
|
32
|
+
*
|
|
33
|
+
* Returns the raw byte sequence. Character encoding conversion is done by
|
|
34
|
+
* the caller (decodeAnsiText).
|
|
35
|
+
*/
|
|
36
|
+
readAnsiBytes(): Uint8Array;
|
|
37
|
+
readAnsiString(): string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Decodes ANSI/Shift_JIS strings. The Japanese version of Touhou replays
|
|
41
|
+
* often stores player names and dates as Shift_JIS byte sequences.
|
|
42
|
+
* raviddog/threplay's ReadStringANSI has a known limitation of turning each
|
|
43
|
+
* byte directly into a char (equivalent to Latin1), which mangles Japanese
|
|
44
|
+
* text (see the ReadStringWide comment in that same implementation). Here we
|
|
45
|
+
* try Shift_JIS first and only fall back to Latin1 if that fails.
|
|
46
|
+
*/
|
|
47
|
+
export declare function decodeAnsiText(bytes: Uint8Array): string;
|
|
48
|
+
//# sourceMappingURL=byte-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byte-reader.d.ts","sourceRoot":"","sources":["../src/byte-reader.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAW;IAChC,GAAG,SAAK;IAER,YAAY,IAAI,EAAE,UAAU,EAG3B;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,OAAO,CAAC,MAAM;IAQd,SAAS,IAAI,MAAM,CAKlB;IAED,YAAY,IAAI,MAAM,CAKrB;IAED,YAAY,IAAI,MAAM,CAKrB;IAED,aAAa,IAAI,MAAM,CAKtB;IAED,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAK/B;IAED,qCAAqC;IACrC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAKzB;IAED,8CAA8C;IAC9C,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAExB;IAED,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAK5C;IAED,SAAS,IAAI,UAAU,CAEtB;IAED,GAAG,IAAI,UAAU,CAEhB;IAED;;;;;;;;;OASG;IACH,aAAa,IAAI,UAAU,CAgB1B;IAED,cAAc,IAAI,MAAM,CAEvB;CACF;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAUxD"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ReplayCorruptError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* A low-level cursor for reading through .rpy binary data.
|
|
4
|
+
* Out-of-range access does not throw a generic exception but a
|
|
5
|
+
* ReplayCorruptError (caught at the parseReplay() boundary and
|
|
6
|
+
* converted to a discriminated error value).
|
|
7
|
+
*/
|
|
8
|
+
export class ByteReader {
|
|
9
|
+
bytes;
|
|
10
|
+
view;
|
|
11
|
+
pos = 0;
|
|
12
|
+
constructor(data) {
|
|
13
|
+
this.bytes = data;
|
|
14
|
+
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
15
|
+
}
|
|
16
|
+
get length() {
|
|
17
|
+
return this.bytes.length;
|
|
18
|
+
}
|
|
19
|
+
ensure(n) {
|
|
20
|
+
if (this.pos < 0 || this.pos + n > this.bytes.length) {
|
|
21
|
+
throw new ReplayCorruptError(`unexpected end of file at offset ${this.pos} (need ${n} more byte(s), have ${this.bytes.length})`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
readUint8() {
|
|
25
|
+
this.ensure(1);
|
|
26
|
+
const value = this.bytes[this.pos];
|
|
27
|
+
this.pos += 1;
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
readUint16LE() {
|
|
31
|
+
this.ensure(2);
|
|
32
|
+
const value = this.view.getUint16(this.pos, true);
|
|
33
|
+
this.pos += 2;
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
readUint32LE() {
|
|
37
|
+
this.ensure(4);
|
|
38
|
+
const value = this.view.getUint32(this.pos, true);
|
|
39
|
+
this.pos += 4;
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
readFloat32LE() {
|
|
43
|
+
this.ensure(4);
|
|
44
|
+
const value = this.view.getFloat32(this.pos, true);
|
|
45
|
+
this.pos += 4;
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
readBytes(n) {
|
|
49
|
+
this.ensure(n);
|
|
50
|
+
const value = this.bytes.subarray(this.pos, this.pos + n);
|
|
51
|
+
this.pos += n;
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/** Jumps to an absolute position. */
|
|
55
|
+
seek(offset) {
|
|
56
|
+
if (offset < 0 || offset > this.bytes.length) {
|
|
57
|
+
throw new ReplayCorruptError(`seek out of range: ${offset} (length ${this.bytes.length})`);
|
|
58
|
+
}
|
|
59
|
+
this.pos = offset;
|
|
60
|
+
}
|
|
61
|
+
/** Moves relative to the current position. */
|
|
62
|
+
skip(delta) {
|
|
63
|
+
this.seek(this.pos + delta);
|
|
64
|
+
}
|
|
65
|
+
slice(start, end) {
|
|
66
|
+
if (start < 0 || end > this.bytes.length || start > end) {
|
|
67
|
+
throw new ReplayCorruptError(`slice out of range: [${start}, ${end}) (length ${this.bytes.length})`);
|
|
68
|
+
}
|
|
69
|
+
return this.bytes.subarray(start, end);
|
|
70
|
+
}
|
|
71
|
+
remaining() {
|
|
72
|
+
return this.bytes.subarray(this.pos);
|
|
73
|
+
}
|
|
74
|
+
all() {
|
|
75
|
+
return this.bytes;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Equivalent to ReadStringANSI in raviddog/threplay. Reads until a
|
|
79
|
+
* CR(0x0D)+LF(0x0A) sequence is found as the string terminator, leaving the
|
|
80
|
+
* cursor just before the terminator (right before the 0x0A), so that
|
|
81
|
+
* subsequent fields' `skip()` offsets match the original implementation
|
|
82
|
+
* (this behavior is faithfully reproduced byte-for-byte).
|
|
83
|
+
*
|
|
84
|
+
* Returns the raw byte sequence. Character encoding conversion is done by
|
|
85
|
+
* the caller (decodeAnsiText).
|
|
86
|
+
*/
|
|
87
|
+
readAnsiBytes() {
|
|
88
|
+
const out = [];
|
|
89
|
+
let b0 = this.readUint8();
|
|
90
|
+
let b1 = this.readUint8();
|
|
91
|
+
if (!(b0 === 13 && b1 === 10)) {
|
|
92
|
+
let b2 = this.readUint8();
|
|
93
|
+
do {
|
|
94
|
+
out.push(b0);
|
|
95
|
+
b0 = b1;
|
|
96
|
+
b1 = b2;
|
|
97
|
+
b2 = this.readUint8();
|
|
98
|
+
} while (b0 !== 13 && b1 !== 10);
|
|
99
|
+
}
|
|
100
|
+
// Equivalent to `file.Seek(-1, SeekOrigin.Current)` in the original implementation.
|
|
101
|
+
this.pos -= 1;
|
|
102
|
+
return Uint8Array.from(out);
|
|
103
|
+
}
|
|
104
|
+
readAnsiString() {
|
|
105
|
+
return decodeAnsiText(this.readAnsiBytes());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Decodes ANSI/Shift_JIS strings. The Japanese version of Touhou replays
|
|
110
|
+
* often stores player names and dates as Shift_JIS byte sequences.
|
|
111
|
+
* raviddog/threplay's ReadStringANSI has a known limitation of turning each
|
|
112
|
+
* byte directly into a char (equivalent to Latin1), which mangles Japanese
|
|
113
|
+
* text (see the ReadStringWide comment in that same implementation). Here we
|
|
114
|
+
* try Shift_JIS first and only fall back to Latin1 if that fails.
|
|
115
|
+
*/
|
|
116
|
+
export function decodeAnsiText(bytes) {
|
|
117
|
+
if (bytes.length === 0)
|
|
118
|
+
return "";
|
|
119
|
+
try {
|
|
120
|
+
const decoded = new TextDecoder("shift_jis", { fatal: true }).decode(bytes);
|
|
121
|
+
return decoded;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// If invalid as Shift_JIS, or the runtime lacks shift_jis support,
|
|
125
|
+
// treat it as Latin1 (1 byte = 1 char), same fallback as the original implementation.
|
|
126
|
+
return Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=byte-reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byte-reader.js","sourceRoot":"","sources":["../src/byte-reader.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An unrecoverable problem encountered while reading a replay (corrupted file,
|
|
3
|
+
* invalid offset, etc.). Thrown only inside decoders and always caught at the
|
|
4
|
+
* parseReplay() boundary, where it is converted to a ReplayParseError. This
|
|
5
|
+
* exception never escapes to callers.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ReplayCorruptError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An unrecoverable problem encountered while reading a replay (corrupted file,
|
|
3
|
+
* invalid offset, etc.). Thrown only inside decoders and always caught at the
|
|
4
|
+
* parseReplay() boundary, where it is converted to a ReplayParseError. This
|
|
5
|
+
* exception never escapes to callers.
|
|
6
|
+
*/
|
|
7
|
+
export class ReplayCorruptError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ReplayCorruptError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title identifiers for Touhou Project main games recognized by this package.
|
|
3
|
+
* Independent of `GameId` in `packages/shared` (this package aims to be
|
|
4
|
+
* publishable standalone as OSS, so it does not depend on Sattori-specific types).
|
|
5
|
+
*
|
|
6
|
+
* th13 (東方神霊廟, TD) and th14 (東方輝針城, DDC) share the same
|
|
7
|
+
* magic bytes `t13r` and are distinguished by a version byte in the header
|
|
8
|
+
* (inherited from threplay; see the in-code comments).
|
|
9
|
+
*/
|
|
10
|
+
export declare const REPLAY_GAME_IDS: readonly ["th06", "th07", "th08", "th09", "th095", "th10", "th11", "th12", "th125", "th128", "th13", "th14", "th143", "th15", "th16", "th165", "th17", "th18", "th20"];
|
|
11
|
+
export type ReplayGameId = (typeof REPLAY_GAME_IDS)[number];
|
|
12
|
+
export declare const REPLAY_GAME_TITLES: Record<ReplayGameId, string>;
|
|
13
|
+
//# sourceMappingURL=game-ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-ids.d.ts","sourceRoot":"","sources":["../src/game-ids.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,YAC1B,MAAM,EACN,MAAM,EACN,MAAM,EACN,MAAM,EACN,OAAO,EACP,MAAM,EACN,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,MAAM,EACN,MAAM,EACN,OAAO,EACP,MAAM,EACN,MAAM,EACN,OAAO,EACP,MAAM,EACN,MAAM,EAGN,MAAM,CACE,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAsB3D,CAAC"}
|
package/dist/game-ids.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title identifiers for Touhou Project main games recognized by this package.
|
|
3
|
+
* Independent of `GameId` in `packages/shared` (this package aims to be
|
|
4
|
+
* publishable standalone as OSS, so it does not depend on Sattori-specific types).
|
|
5
|
+
*
|
|
6
|
+
* th13 (東方神霊廟, TD) and th14 (東方輝針城, DDC) share the same
|
|
7
|
+
* magic bytes `t13r` and are distinguished by a version byte in the header
|
|
8
|
+
* (inherited from threplay; see the in-code comments).
|
|
9
|
+
*/
|
|
10
|
+
export const REPLAY_GAME_IDS = [
|
|
11
|
+
"th06",
|
|
12
|
+
"th07",
|
|
13
|
+
"th08",
|
|
14
|
+
"th09",
|
|
15
|
+
"th095",
|
|
16
|
+
"th10",
|
|
17
|
+
"th11",
|
|
18
|
+
"th12",
|
|
19
|
+
"th125",
|
|
20
|
+
"th128",
|
|
21
|
+
"th13",
|
|
22
|
+
"th14",
|
|
23
|
+
"th143",
|
|
24
|
+
"th15",
|
|
25
|
+
"th16",
|
|
26
|
+
"th165",
|
|
27
|
+
"th17",
|
|
28
|
+
"th18",
|
|
29
|
+
// th19 (東方獣王園, UDoALG) is excluded because it has no replay-saving feature
|
|
30
|
+
// "th19",
|
|
31
|
+
"th20",
|
|
32
|
+
];
|
|
33
|
+
export const REPLAY_GAME_TITLES = {
|
|
34
|
+
th06: "東方紅魔郷 ~ the Embodiment of Scarlet Devil.",
|
|
35
|
+
th07: "東方妖々夢 ~ Perfect Cherry Blossom.",
|
|
36
|
+
th08: "東方永夜抄 ~ Imperishable Night.",
|
|
37
|
+
th09: "東方花映塚 ~ Phantasmagoria of Flower View.",
|
|
38
|
+
th095: "東方文花帖 ~ Shoot the Bullet.",
|
|
39
|
+
th10: "東方風神録 ~ Mountain of Faith.",
|
|
40
|
+
th11: "東方地霊殿 ~ Subterranean Animism.",
|
|
41
|
+
th12: "東方星蓮船 ~ Undefined Fantastic Object.",
|
|
42
|
+
th125: "ダブルスポイラー ~ 東方文花帖",
|
|
43
|
+
th128: "妖精大戦争 ~ 東方三月精",
|
|
44
|
+
th13: "東方神霊廟 ~ Ten Desires.",
|
|
45
|
+
th14: "東方輝針城 ~ Double Dealing Character.",
|
|
46
|
+
th143: "弾幕アマノジャク ~ Impossible Spell Card.",
|
|
47
|
+
th15: "東方紺珠伝 ~ Legacy of Lunatic Kingdom.",
|
|
48
|
+
th16: "東方天空璋 ~ Hidden Star in Four Seasons.",
|
|
49
|
+
th165: "秘封ナイトメアダイアリー ~ Violet Detector.",
|
|
50
|
+
th17: "東方鬼形獣 ~ Wily Beast and Weakest Creature.",
|
|
51
|
+
th18: "東方虹龍洞 ~ Unconnected Marketeers.",
|
|
52
|
+
// th19 (東方獣王園, UDoALG) is excluded because it has no replay-saving feature
|
|
53
|
+
// th19: "東方獣王園 〜 Unfinished Dream of All Living Ghost.",
|
|
54
|
+
th20: "東方錦上京 ~ Fossilized Wonders.",
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=game-ids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-ids.js","sourceRoot":"","sources":["../src/game-ids.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;IACP,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,2EAA2E;IAC3E,UAAU;IACV,MAAM;CACE,CAAC;AAIX,MAAM,CAAC,MAAM,kBAAkB,GAAiC;IAC9D,IAAI,EAAE,0CAA0C;IAChD,IAAI,EAAE,iCAAiC;IACvC,IAAI,EAAE,6BAA6B;IACnC,IAAI,EAAE,wCAAwC;IAC9C,KAAK,EAAE,2BAA2B;IAClC,IAAI,EAAE,4BAA4B;IAClC,IAAI,EAAE,+BAA+B;IACrC,IAAI,EAAE,qCAAqC;IAC3C,KAAK,EAAE,kBAAkB;IACzB,KAAK,EAAE,eAAe;IACtB,IAAI,EAAE,sBAAsB;IAC5B,IAAI,EAAE,mCAAmC;IACzC,KAAK,EAAE,mCAAmC;IAC1C,IAAI,EAAE,oCAAoC;IAC1C,IAAI,EAAE,sCAAsC;IAC5C,KAAK,EAAE,iCAAiC;IACxC,IAAI,EAAE,0CAA0C;IAChD,IAAI,EAAE,iCAAiC;IACvC,2EAA2E;IAC3E,yDAAyD;IACzD,IAAI,EAAE,6BAA6B;CACpC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "body" decoding pipeline shared from th10 (東方風神録, MoF) onward.
|
|
3
|
+
* After stripping the 36-byte header, this runs two passes of XOR block
|
|
4
|
+
* decoding followed by LZSS decompression (consolidating the processing
|
|
5
|
+
* common to the start of each Read_tNNr in threplay).
|
|
6
|
+
*/
|
|
7
|
+
export declare function decodeModernBody(original: Uint8Array, pass1: {
|
|
8
|
+
blockSize: number;
|
|
9
|
+
base: number;
|
|
10
|
+
add: number;
|
|
11
|
+
}, pass2: {
|
|
12
|
+
blockSize: number;
|
|
13
|
+
base: number;
|
|
14
|
+
add: number;
|
|
15
|
+
}): Uint8Array;
|
|
16
|
+
//# sourceMappingURL=modern-body.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modern-body.d.ts","sourceRoot":"","sources":["../../src/games/modern-body.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACvD,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACtD,UAAU,CAUZ"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ReplayCorruptError } from "../errors.js";
|
|
2
|
+
import { decompress, readBufferedUint32LE, xorBlockDecode } from "../lzss.js";
|
|
3
|
+
const MODERN_HEADER_SIZE = 36;
|
|
4
|
+
/**
|
|
5
|
+
* The "body" decoding pipeline shared from th10 (東方風神録, MoF) onward.
|
|
6
|
+
* After stripping the 36-byte header, this runs two passes of XOR block
|
|
7
|
+
* decoding followed by LZSS decompression (consolidating the processing
|
|
8
|
+
* common to the start of each Read_tNNr in threplay).
|
|
9
|
+
*/
|
|
10
|
+
export function decodeModernBody(original, pass1, pass2) {
|
|
11
|
+
if (original.length < MODERN_HEADER_SIZE + 4 + 4) {
|
|
12
|
+
throw new ReplayCorruptError("file too short for modern-era header");
|
|
13
|
+
}
|
|
14
|
+
const length = readBufferedUint32LE(original, 28);
|
|
15
|
+
const dlength = readBufferedUint32LE(original, 32);
|
|
16
|
+
const workBuffer = original.slice(MODERN_HEADER_SIZE);
|
|
17
|
+
xorBlockDecode(workBuffer, length, pass1.blockSize, pass1.base, pass1.add);
|
|
18
|
+
xorBlockDecode(workBuffer, length, pass2.blockSize, pass2.base, pass2.add);
|
|
19
|
+
return decompress(workBuffer, length, dlength);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=modern-body.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modern-body.js","sourceRoot":"","sources":["../../src/games/modern-body.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAoB,EACpB,KAAuD,EACvD,KAAuD;IAEvD,IAAI,QAAQ,CAAC,MAAM,GAAG,kBAAkB,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,kBAAkB,CAAC,sCAAsC,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,MAAM,GAAG,oBAAoB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,oBAAoB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACtD,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3E,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3E,OAAO,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ParsedReplay } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* T6RP (東方紅魔郷, EoSD) decoder. Ported from Read_T6RP in threplay.
|
|
4
|
+
* The header is decoded with a simple additive-key scheme (subtracting a key
|
|
5
|
+
* that is updated by +7 each step).
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseTh06(original: Uint8Array): ParsedReplay;
|
|
8
|
+
//# sourceMappingURL=th06.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"th06.d.ts","sourceRoot":"","sources":["../../src/games/th06.ts"],"names":[],"mappings":"AAGA,OAAO,EAA4C,KAAK,YAAY,EAAyB,MAAM,aAAa,CAAC;AAkBjH;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,UAAU,GAAG,YAAY,CAqD5D"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { decodeAnsiText } from "../byte-reader.js";
|
|
2
|
+
import { ReplayCorruptError } from "../errors.js";
|
|
3
|
+
import { additiveKeyDecode, readBufferedUint32LE } from "../lzss.js";
|
|
4
|
+
import { emptySplit, normalizeText, resourceCount } from "../types.js";
|
|
5
|
+
import { REPLAY_GAME_TITLES } from "../game-ids.js";
|
|
6
|
+
const CHARACTERS = ["ReimuA", "ReimuB", "MarisaA", "MarisaB"];
|
|
7
|
+
const DIFFICULTIES = ["Easy", "Normal", "Hard", "Lunatic", "Extra"];
|
|
8
|
+
function readNullTerminatedAnsi(buffer, start) {
|
|
9
|
+
let end = start;
|
|
10
|
+
while (true) {
|
|
11
|
+
if (end >= buffer.length) {
|
|
12
|
+
throw new ReplayCorruptError(`unterminated string starting at ${start}`);
|
|
13
|
+
}
|
|
14
|
+
if (buffer[end] === 0x00)
|
|
15
|
+
break;
|
|
16
|
+
end++;
|
|
17
|
+
}
|
|
18
|
+
return { text: decodeAnsiText(buffer.subarray(start, end)), end };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* T6RP (東方紅魔郷, EoSD) decoder. Ported from Read_T6RP in threplay.
|
|
22
|
+
* The header is decoded with a simple additive-key scheme (subtracting a key
|
|
23
|
+
* that is updated by +7 each step).
|
|
24
|
+
*/
|
|
25
|
+
export function parseTh06(original) {
|
|
26
|
+
if (original.length < 0x38) {
|
|
27
|
+
throw new ReplayCorruptError("file too short for T6RP header");
|
|
28
|
+
}
|
|
29
|
+
const buffer = original.slice();
|
|
30
|
+
const characterIndex = buffer[0x06];
|
|
31
|
+
const difficultyIndex = buffer[0x07];
|
|
32
|
+
const character = CHARACTERS[characterIndex] ?? null;
|
|
33
|
+
const difficulty = DIFFICULTIES[difficultyIndex] ?? null;
|
|
34
|
+
additiveKeyDecode(buffer, 0x0f, buffer[0x0e], 7);
|
|
35
|
+
const { text: date, end: afterDate } = readNullTerminatedAnsi(buffer, 0x10);
|
|
36
|
+
const { text: name } = readNullTerminatedAnsi(buffer, Math.max(afterDate + 1, 0x19));
|
|
37
|
+
const score = readBufferedUint32LE(buffer, 0x24);
|
|
38
|
+
const scoreOffsets = [];
|
|
39
|
+
let maxStage = 0;
|
|
40
|
+
for (let i = 0; i < 7; i++) {
|
|
41
|
+
const offset = readBufferedUint32LE(buffer, 0x34 + 4 * i);
|
|
42
|
+
scoreOffsets.push(offset);
|
|
43
|
+
if (offset !== 0)
|
|
44
|
+
maxStage = i;
|
|
45
|
+
}
|
|
46
|
+
const splits = [];
|
|
47
|
+
if (maxStage === 6) {
|
|
48
|
+
const offset = scoreOffsets[6];
|
|
49
|
+
const split = readStageSplit(buffer, offset, 7);
|
|
50
|
+
splits.push(split);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
for (let i = 0; i <= maxStage; i++) {
|
|
54
|
+
const offset = scoreOffsets[i];
|
|
55
|
+
if (offset === 0)
|
|
56
|
+
continue;
|
|
57
|
+
splits.push(readStageSplit(buffer, offset, i + 1));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
game: "th06",
|
|
62
|
+
gameTitle: REPLAY_GAME_TITLES.th06,
|
|
63
|
+
formatVersion: null,
|
|
64
|
+
player: normalizeText(name),
|
|
65
|
+
date: normalizeText(date),
|
|
66
|
+
character,
|
|
67
|
+
difficulty,
|
|
68
|
+
stage: null,
|
|
69
|
+
score,
|
|
70
|
+
cleared: maxStage === 6,
|
|
71
|
+
splits,
|
|
72
|
+
frameCount: null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function readStageSplit(buffer, offset, stage) {
|
|
76
|
+
const split = emptySplit();
|
|
77
|
+
split.stage = stage;
|
|
78
|
+
split.score = readBufferedUint32LE(buffer, offset);
|
|
79
|
+
split.power = String(buffer[offset + 0x8] ?? 0);
|
|
80
|
+
split.lives = resourceCount(buffer[offset + 0x9] ?? 0);
|
|
81
|
+
split.bombs = resourceCount(buffer[offset + 0xa] ?? 0);
|
|
82
|
+
split.additional = { rank: buffer[offset + 0xb] ?? 0 };
|
|
83
|
+
return split;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=th06.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"th06.js","sourceRoot":"","sources":["../../src/games/th06.ts"],"names":[],"mappings":"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"}
|