@hax-brasil/replay-decoder 1.0.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 +77 -0
- package/index.d.ts +29 -0
- package/index.js +294 -0
- package/package.json +115 -0
- package/types.d.ts +443 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 N-API for Rust
|
|
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,77 @@
|
|
|
1
|
+
# @hax-brasil/replay-decoder
|
|
2
|
+
|
|
3
|
+
Node.js native decoder and validator for Haxball `HBR2` replay files.
|
|
4
|
+
|
|
5
|
+
This package wraps [`haxball-replay-decoder`](https://crates.io/crates/haxball-replay-decoder) with N-API and exposes:
|
|
6
|
+
|
|
7
|
+
- sync + async replay decoding
|
|
8
|
+
- sync + async replay validation
|
|
9
|
+
- rich structured decode errors
|
|
10
|
+
- full TypeScript types for decoded replay structures and validation reports
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @hax-brasil/replay-decoder
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { decode, validate, tryDecode, ReplayDecodeError } from '@hax-brasil/replay-decoder'
|
|
22
|
+
import { readFileSync } from 'node:fs'
|
|
23
|
+
|
|
24
|
+
const bytes = readFileSync('./recording.hbr2')
|
|
25
|
+
|
|
26
|
+
const replay = decode(bytes)
|
|
27
|
+
console.log(replay.version, replay.totalFrames)
|
|
28
|
+
|
|
29
|
+
const report = validate(bytes, 'strict')
|
|
30
|
+
console.log(report.issues)
|
|
31
|
+
|
|
32
|
+
const safe = tryDecode(bytes)
|
|
33
|
+
if (!safe.ok) {
|
|
34
|
+
console.error(safe.error.kind, safe.error.details)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
decode(Buffer.from('bad'))
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof ReplayDecodeError) {
|
|
41
|
+
console.error(error.kind, error.details)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
- `decode(bytes, options?) => ReplayData`
|
|
49
|
+
- `decodeAsync(bytes, options?) => Promise<ReplayData>`
|
|
50
|
+
- `tryDecode(bytes, options?) => DecodeResult`
|
|
51
|
+
- `tryDecodeAsync(bytes, options?) => Promise<DecodeResult>`
|
|
52
|
+
- `validate(bytes, profile?) => ValidationReport`
|
|
53
|
+
- `validateAsync(bytes, profile?) => Promise<ValidationReport>`
|
|
54
|
+
|
|
55
|
+
### Input
|
|
56
|
+
|
|
57
|
+
All APIs accept only bytes:
|
|
58
|
+
|
|
59
|
+
- `Buffer`
|
|
60
|
+
- `Uint8Array`
|
|
61
|
+
|
|
62
|
+
### Decode options
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
interface DecodeOptions {
|
|
66
|
+
validationProfile?: 'strict' | 'structural'
|
|
67
|
+
allowUnknownEventTypes?: boolean
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pnpm install
|
|
75
|
+
pnpm build
|
|
76
|
+
pnpm test
|
|
77
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export * from './types'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
BytesLike,
|
|
5
|
+
DecodeFailure,
|
|
6
|
+
DecodeOptions,
|
|
7
|
+
DecodeResult,
|
|
8
|
+
DecodeErrorDetails,
|
|
9
|
+
DecodeErrorKind,
|
|
10
|
+
ReplayData,
|
|
11
|
+
ValidationProfile,
|
|
12
|
+
ValidationReport,
|
|
13
|
+
} from './types'
|
|
14
|
+
|
|
15
|
+
export declare class ReplayDecodeError extends Error {
|
|
16
|
+
name: 'ReplayDecodeError'
|
|
17
|
+
kind: DecodeErrorKind
|
|
18
|
+
details: DecodeErrorDetails
|
|
19
|
+
constructor(error: DecodeFailure)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare function decode(bytes: BytesLike, options?: DecodeOptions): ReplayData
|
|
23
|
+
export declare function decodeAsync(bytes: BytesLike, options?: DecodeOptions): Promise<ReplayData>
|
|
24
|
+
|
|
25
|
+
export declare function tryDecode(bytes: BytesLike, options?: DecodeOptions): DecodeResult
|
|
26
|
+
export declare function tryDecodeAsync(bytes: BytesLike, options?: DecodeOptions): Promise<DecodeResult>
|
|
27
|
+
|
|
28
|
+
export declare function validate(bytes: BytesLike, profile?: ValidationProfile): ValidationReport
|
|
29
|
+
export declare function validateAsync(bytes: BytesLike, profile?: ValidationProfile): Promise<ValidationReport>
|
package/index.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// prettier-ignore
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
const { readFileSync } = require('node:fs')
|
|
5
|
+
|
|
6
|
+
const loadErrors = []
|
|
7
|
+
|
|
8
|
+
const isMusl = () => {
|
|
9
|
+
let musl = false
|
|
10
|
+
if (process.platform === 'linux') {
|
|
11
|
+
musl = isMuslFromFilesystem()
|
|
12
|
+
if (musl === null) {
|
|
13
|
+
musl = isMuslFromReport()
|
|
14
|
+
}
|
|
15
|
+
if (musl === null) {
|
|
16
|
+
musl = isMuslFromChildProcess()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return musl
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isFileMusl = (file) => file.includes('libc.musl-') || file.includes('ld-musl-')
|
|
23
|
+
|
|
24
|
+
const isMuslFromFilesystem = () => {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isMuslFromReport = () => {
|
|
33
|
+
let report = null
|
|
34
|
+
if (typeof process.report?.getReport === 'function') {
|
|
35
|
+
process.report.excludeNetwork = true
|
|
36
|
+
report = process.report.getReport()
|
|
37
|
+
}
|
|
38
|
+
if (!report) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
if (report.header && report.header.glibcVersionRuntime) {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(report.sharedObjects)) {
|
|
45
|
+
if (report.sharedObjects.some(isFileMusl)) {
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const isMuslFromChildProcess = () => {
|
|
53
|
+
try {
|
|
54
|
+
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const makeCandidates = () => {
|
|
61
|
+
const candidates = []
|
|
62
|
+
const add = (localFile, packageName) => {
|
|
63
|
+
candidates.push({ type: 'local', id: localFile })
|
|
64
|
+
candidates.push({ type: 'package', id: packageName })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { platform, arch } = process
|
|
68
|
+
|
|
69
|
+
if (platform === 'win32') {
|
|
70
|
+
if (arch === 'x64') {
|
|
71
|
+
add('./replay-decoder.win32-x64-msvc.node', '@hax-brasil/replay-decoder-win32-x64-msvc')
|
|
72
|
+
return candidates
|
|
73
|
+
}
|
|
74
|
+
if (arch === 'arm64') {
|
|
75
|
+
add('./replay-decoder.win32-arm64-msvc.node', '@hax-brasil/replay-decoder-win32-arm64-msvc')
|
|
76
|
+
return candidates
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Unsupported Windows architecture: ${arch}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (platform === 'darwin') {
|
|
82
|
+
add('./replay-decoder.darwin-universal.node', '@hax-brasil/replay-decoder-darwin-universal')
|
|
83
|
+
|
|
84
|
+
if (arch === 'x64') {
|
|
85
|
+
add('./replay-decoder.darwin-x64.node', '@hax-brasil/replay-decoder-darwin-x64')
|
|
86
|
+
return candidates
|
|
87
|
+
}
|
|
88
|
+
if (arch === 'arm64') {
|
|
89
|
+
add('./replay-decoder.darwin-arm64.node', '@hax-brasil/replay-decoder-darwin-arm64')
|
|
90
|
+
return candidates
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Unsupported macOS architecture: ${arch}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (platform === 'linux') {
|
|
96
|
+
const musl = isMusl()
|
|
97
|
+
|
|
98
|
+
if (arch === 'x64') {
|
|
99
|
+
if (musl) {
|
|
100
|
+
add('./replay-decoder.linux-x64-musl.node', '@hax-brasil/replay-decoder-linux-x64-musl')
|
|
101
|
+
}
|
|
102
|
+
add('./replay-decoder.linux-x64-gnu.node', '@hax-brasil/replay-decoder-linux-x64-gnu')
|
|
103
|
+
return candidates
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (arch === 'arm64') {
|
|
107
|
+
if (musl) {
|
|
108
|
+
add('./replay-decoder.linux-arm64-musl.node', '@hax-brasil/replay-decoder-linux-arm64-musl')
|
|
109
|
+
}
|
|
110
|
+
add('./replay-decoder.linux-arm64-gnu.node', '@hax-brasil/replay-decoder-linux-arm64-gnu')
|
|
111
|
+
return candidates
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw new Error(`Unsupported Linux architecture: ${arch}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const loadNativeBinding = () => {
|
|
121
|
+
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
|
122
|
+
try {
|
|
123
|
+
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
loadErrors.push(error)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let candidates = []
|
|
130
|
+
try {
|
|
131
|
+
candidates = makeCandidates()
|
|
132
|
+
} catch (error) {
|
|
133
|
+
loadErrors.push(error)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const candidate of candidates) {
|
|
137
|
+
try {
|
|
138
|
+
return require(candidate.id)
|
|
139
|
+
} catch (error) {
|
|
140
|
+
loadErrors.push(error)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (loadErrors.length > 0) {
|
|
145
|
+
throw new Error('Failed to load native @hax-brasil/replay-decoder binding', {
|
|
146
|
+
cause: loadErrors.reduce((error, current) => {
|
|
147
|
+
current.cause = error
|
|
148
|
+
return current
|
|
149
|
+
}),
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error('Native binding was not loaded')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const nativeBinding = loadNativeBinding()
|
|
157
|
+
|
|
158
|
+
class ReplayDecodeError extends Error {
|
|
159
|
+
constructor(error) {
|
|
160
|
+
super(error.message)
|
|
161
|
+
this.name = 'ReplayDecodeError'
|
|
162
|
+
this.kind = error.kind
|
|
163
|
+
this.details = error.details
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const normalizeBytesLike = (bytes) => {
|
|
168
|
+
if (Buffer.isBuffer(bytes)) {
|
|
169
|
+
return bytes
|
|
170
|
+
}
|
|
171
|
+
if (bytes instanceof Uint8Array) {
|
|
172
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
173
|
+
}
|
|
174
|
+
throw new TypeError('Expected bytes to be a Buffer or Uint8Array')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const normalizeValidationProfile = (profile) => {
|
|
178
|
+
if (profile == null) {
|
|
179
|
+
return 'strict'
|
|
180
|
+
}
|
|
181
|
+
if (profile === 'strict' || profile === 'structural') {
|
|
182
|
+
return profile
|
|
183
|
+
}
|
|
184
|
+
throw new TypeError("validationProfile must be 'strict' or 'structural'")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const normalizeDecodeOptions = (options) => {
|
|
188
|
+
if (options == null) {
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (typeof options !== 'object' || Array.isArray(options)) {
|
|
193
|
+
throw new TypeError('decode options must be an object')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const normalized = {}
|
|
197
|
+
|
|
198
|
+
if (Object.hasOwn(options, 'validationProfile') && options.validationProfile !== undefined) {
|
|
199
|
+
normalized.validationProfile = normalizeValidationProfile(options.validationProfile)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (Object.hasOwn(options, 'allowUnknownEventTypes') && options.allowUnknownEventTypes !== undefined) {
|
|
203
|
+
if (typeof options.allowUnknownEventTypes !== 'boolean') {
|
|
204
|
+
throw new TypeError('allowUnknownEventTypes must be a boolean')
|
|
205
|
+
}
|
|
206
|
+
normalized.allowUnknownEventTypes = options.allowUnknownEventTypes
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (Object.keys(normalized).length === 0) {
|
|
210
|
+
return undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return normalized
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const parseNativeJson = (raw, apiName) => {
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(raw)
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new Error(`Native ${apiName} returned invalid JSON`, { cause: error })
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const decodeTrySyncInternal = (bytes, options) => {
|
|
225
|
+
const normalizedBytes = normalizeBytesLike(bytes)
|
|
226
|
+
const normalizedOptions = normalizeDecodeOptions(options)
|
|
227
|
+
const optionsJson = normalizedOptions ? JSON.stringify(normalizedOptions) : undefined
|
|
228
|
+
const raw = nativeBinding.__decode_try_json_sync(normalizedBytes, optionsJson)
|
|
229
|
+
return parseNativeJson(raw, '__decode_try_json_sync')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const decodeTryAsyncInternal = async (bytes, options) => {
|
|
233
|
+
const normalizedBytes = normalizeBytesLike(bytes)
|
|
234
|
+
const normalizedOptions = normalizeDecodeOptions(options)
|
|
235
|
+
const optionsJson = normalizedOptions ? JSON.stringify(normalizedOptions) : undefined
|
|
236
|
+
const raw = await nativeBinding.__decode_try_json_async(normalizedBytes, optionsJson)
|
|
237
|
+
return parseNativeJson(raw, '__decode_try_json_async')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const validateSyncInternal = (bytes, profile) => {
|
|
241
|
+
const normalizedBytes = normalizeBytesLike(bytes)
|
|
242
|
+
const normalizedProfile = normalizeValidationProfile(profile)
|
|
243
|
+
const raw = nativeBinding.__validate_json_sync(normalizedBytes, normalizedProfile)
|
|
244
|
+
return parseNativeJson(raw, '__validate_json_sync')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const validateAsyncInternal = async (bytes, profile) => {
|
|
248
|
+
const normalizedBytes = normalizeBytesLike(bytes)
|
|
249
|
+
const normalizedProfile = normalizeValidationProfile(profile)
|
|
250
|
+
const raw = await nativeBinding.__validate_json_async(normalizedBytes, normalizedProfile)
|
|
251
|
+
return parseNativeJson(raw, '__validate_json_async')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const decode = (bytes, options) => {
|
|
255
|
+
const result = decodeTrySyncInternal(bytes, options)
|
|
256
|
+
if (result.ok) {
|
|
257
|
+
return result.data
|
|
258
|
+
}
|
|
259
|
+
throw new ReplayDecodeError(result.error)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const decodeAsync = async (bytes, options) => {
|
|
263
|
+
const result = await decodeTryAsyncInternal(bytes, options)
|
|
264
|
+
if (result.ok) {
|
|
265
|
+
return result.data
|
|
266
|
+
}
|
|
267
|
+
throw new ReplayDecodeError(result.error)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const tryDecode = (bytes, options) => decodeTrySyncInternal(bytes, options)
|
|
271
|
+
|
|
272
|
+
const tryDecodeAsync = async (bytes, options) => decodeTryAsyncInternal(bytes, options)
|
|
273
|
+
|
|
274
|
+
const validate = (bytes, profile) => validateSyncInternal(bytes, profile)
|
|
275
|
+
|
|
276
|
+
const validateAsync = async (bytes, profile) => validateAsyncInternal(bytes, profile)
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
ReplayDecodeError,
|
|
280
|
+
decode,
|
|
281
|
+
decodeAsync,
|
|
282
|
+
tryDecode,
|
|
283
|
+
tryDecodeAsync,
|
|
284
|
+
validate,
|
|
285
|
+
validateAsync,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports.ReplayDecodeError = ReplayDecodeError
|
|
289
|
+
module.exports.decode = decode
|
|
290
|
+
module.exports.decodeAsync = decodeAsync
|
|
291
|
+
module.exports.tryDecode = tryDecode
|
|
292
|
+
module.exports.tryDecodeAsync = tryDecodeAsync
|
|
293
|
+
module.exports.validate = validate
|
|
294
|
+
module.exports.validateAsync = validateAsync
|
package/package.json
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hax-brasil/replay-decoder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node.js HBR2 decoder and validator powered by haxball-replay-decoder",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hax-brasil/node-haxball-replay-decoder.git"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"haxball",
|
|
14
|
+
"replay",
|
|
15
|
+
"hbr2",
|
|
16
|
+
"decoder",
|
|
17
|
+
"napi-rs",
|
|
18
|
+
"node-addon"
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
"index.d.ts",
|
|
22
|
+
"types.d.ts",
|
|
23
|
+
"index.js",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"napi": {
|
|
27
|
+
"binaryName": "replay-decoder",
|
|
28
|
+
"targets": [
|
|
29
|
+
"x86_64-pc-windows-msvc",
|
|
30
|
+
"x86_64-apple-darwin",
|
|
31
|
+
"x86_64-unknown-linux-gnu",
|
|
32
|
+
"aarch64-unknown-linux-gnu",
|
|
33
|
+
"aarch64-apple-darwin"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">= 16"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"registry": "https://registry.npmjs.org/",
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"artifacts": "napi artifacts",
|
|
45
|
+
"bench": "node --import @oxc-node/core/register benchmark/bench.ts",
|
|
46
|
+
"build": "napi build --platform --release --no-js --dts native.d.ts",
|
|
47
|
+
"build:debug": "napi build --platform --no-js --dts native.d.ts",
|
|
48
|
+
"format": "run-p format:prettier format:rs format:toml",
|
|
49
|
+
"format:prettier": "prettier . -w",
|
|
50
|
+
"format:toml": "taplo format",
|
|
51
|
+
"format:rs": "cargo fmt",
|
|
52
|
+
"lint": "oxlint .",
|
|
53
|
+
"prepublishOnly": "napi prepublish -t npm",
|
|
54
|
+
"test": "pnpm typecheck && pnpm build:debug && ava",
|
|
55
|
+
"typecheck": "tsc -p __test__/tsconfig.json --noEmit",
|
|
56
|
+
"preversion": "napi build --platform --no-js --dts native.d.ts && git add .",
|
|
57
|
+
"version": "napi version",
|
|
58
|
+
"prepare": "husky"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^24.3.0",
|
|
62
|
+
"@napi-rs/cli": "^3.2.0",
|
|
63
|
+
"@oxc-node/core": "^0.0.35",
|
|
64
|
+
"@taplo/cli": "^0.7.0",
|
|
65
|
+
"ava": "^7.0.0",
|
|
66
|
+
"chalk": "^5.6.2",
|
|
67
|
+
"husky": "^9.1.7",
|
|
68
|
+
"lint-staged": "^16.1.6",
|
|
69
|
+
"npm-run-all2": "^8.0.4",
|
|
70
|
+
"oxlint": "^1.14.0",
|
|
71
|
+
"prettier": "^3.6.2",
|
|
72
|
+
"tinybench": "^6.0.0",
|
|
73
|
+
"typescript": "^5.9.2"
|
|
74
|
+
},
|
|
75
|
+
"lint-staged": {
|
|
76
|
+
"*.@(js|ts|tsx)": [
|
|
77
|
+
"oxlint --fix"
|
|
78
|
+
],
|
|
79
|
+
"*.@(js|ts|tsx|yml|yaml|md|json)": [
|
|
80
|
+
"prettier --write"
|
|
81
|
+
],
|
|
82
|
+
"*.toml": [
|
|
83
|
+
"taplo format"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"ava": {
|
|
87
|
+
"extensions": {
|
|
88
|
+
"ts": "module"
|
|
89
|
+
},
|
|
90
|
+
"timeout": "2m",
|
|
91
|
+
"workerThreads": false,
|
|
92
|
+
"environmentVariables": {
|
|
93
|
+
"OXC_TSCONFIG_PATH": "./__test__/tsconfig.json"
|
|
94
|
+
},
|
|
95
|
+
"nodeArguments": [
|
|
96
|
+
"--import",
|
|
97
|
+
"@oxc-node/core/register"
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"prettier": {
|
|
101
|
+
"printWidth": 120,
|
|
102
|
+
"semi": false,
|
|
103
|
+
"trailingComma": "all",
|
|
104
|
+
"singleQuote": true,
|
|
105
|
+
"arrowParens": "always"
|
|
106
|
+
},
|
|
107
|
+
"packageManager": "pnpm@10.13.1",
|
|
108
|
+
"optionalDependencies": {
|
|
109
|
+
"@hax-brasil/replay-decoder-win32-x64-msvc": "1.0.0",
|
|
110
|
+
"@hax-brasil/replay-decoder-darwin-x64": "1.0.0",
|
|
111
|
+
"@hax-brasil/replay-decoder-linux-x64-gnu": "1.0.0",
|
|
112
|
+
"@hax-brasil/replay-decoder-linux-arm64-gnu": "1.0.0",
|
|
113
|
+
"@hax-brasil/replay-decoder-darwin-arm64": "1.0.0"
|
|
114
|
+
}
|
|
115
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
export type BytesLike = Buffer | Uint8Array
|
|
2
|
+
|
|
3
|
+
export type ValidationProfile = 'strict' | 'structural'
|
|
4
|
+
|
|
5
|
+
export interface DecodeOptions {
|
|
6
|
+
validationProfile?: ValidationProfile
|
|
7
|
+
allowUnknownEventTypes?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ValidationSeverity = 'error' | 'warning'
|
|
11
|
+
|
|
12
|
+
export interface ValidationIssue {
|
|
13
|
+
code: string
|
|
14
|
+
severity: ValidationSeverity
|
|
15
|
+
path: string
|
|
16
|
+
message: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ValidationReport {
|
|
20
|
+
profile: ValidationProfile
|
|
21
|
+
issues: ValidationIssue[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type OperationType =
|
|
25
|
+
| 'sendAnnouncement'
|
|
26
|
+
| 'sendChatIndicator'
|
|
27
|
+
| 'checkConsistency'
|
|
28
|
+
| 'sendInput'
|
|
29
|
+
| 'sendChat'
|
|
30
|
+
| 'joinRoom'
|
|
31
|
+
| 'kickBanPlayer'
|
|
32
|
+
| 'startGame'
|
|
33
|
+
| 'stopGame'
|
|
34
|
+
| 'pauseResumeGame'
|
|
35
|
+
| 'setGamePlayLimit'
|
|
36
|
+
| 'setStadium'
|
|
37
|
+
| 'setPlayerTeam'
|
|
38
|
+
| 'setTeamsLock'
|
|
39
|
+
| 'setPlayerAdmin'
|
|
40
|
+
| 'autoTeams'
|
|
41
|
+
| 'setPlayerSync'
|
|
42
|
+
| 'ping'
|
|
43
|
+
| 'setAvatar'
|
|
44
|
+
| 'setTeamColors'
|
|
45
|
+
| 'reorderPlayers'
|
|
46
|
+
| 'setKickRateLimit'
|
|
47
|
+
| 'setHeadlessAvatar'
|
|
48
|
+
| 'setDiscProperties'
|
|
49
|
+
| 'customEvent'
|
|
50
|
+
| 'binaryCustomEvent'
|
|
51
|
+
| 'setPlayerIdentity'
|
|
52
|
+
|
|
53
|
+
export type BackgroundType = 'none' | 'grass' | 'hockey' | { unknown: number }
|
|
54
|
+
|
|
55
|
+
export type CameraFollow = 'none' | 'player' | { unknown: number }
|
|
56
|
+
|
|
57
|
+
export type GamePlayState = 'beforeKickOff' | 'playing' | 'afterGoal' | 'ending' | { unknown: number }
|
|
58
|
+
|
|
59
|
+
export interface Point {
|
|
60
|
+
x: number
|
|
61
|
+
y: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Vertex {
|
|
65
|
+
pos: Point
|
|
66
|
+
bCoef: number
|
|
67
|
+
cMask: number
|
|
68
|
+
cGroup: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Segment {
|
|
72
|
+
flags: number
|
|
73
|
+
v0: number
|
|
74
|
+
v1: number
|
|
75
|
+
bias: number
|
|
76
|
+
curve: number
|
|
77
|
+
color: number
|
|
78
|
+
vis: boolean
|
|
79
|
+
bCoef: number
|
|
80
|
+
cMask: number
|
|
81
|
+
cGroup: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface Plane {
|
|
85
|
+
normal: Point
|
|
86
|
+
dist: number
|
|
87
|
+
bCoef: number
|
|
88
|
+
cMask: number
|
|
89
|
+
cGroup: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface Goal {
|
|
93
|
+
p0: Point
|
|
94
|
+
p1: Point
|
|
95
|
+
teamId: number
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface Disc {
|
|
99
|
+
pos: Point
|
|
100
|
+
speed: Point
|
|
101
|
+
gravity: Point
|
|
102
|
+
radius: number
|
|
103
|
+
bCoef: number
|
|
104
|
+
invMass: number
|
|
105
|
+
damping: number
|
|
106
|
+
color: number
|
|
107
|
+
cMask: number
|
|
108
|
+
cGroup: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface Joint {
|
|
112
|
+
d0: number
|
|
113
|
+
d1: number
|
|
114
|
+
minLength: number
|
|
115
|
+
maxLength: number
|
|
116
|
+
strength: number
|
|
117
|
+
color: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PlayerPhysics {
|
|
121
|
+
bCoef: number
|
|
122
|
+
invMass: number
|
|
123
|
+
damping: number
|
|
124
|
+
acceleration: number
|
|
125
|
+
kickingAcceleration: number
|
|
126
|
+
kickingDamping: number
|
|
127
|
+
kickStrength: number
|
|
128
|
+
gravity: Point
|
|
129
|
+
cGroup: number
|
|
130
|
+
radius: number
|
|
131
|
+
kickback: number
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface TeamColors {
|
|
135
|
+
angle: number
|
|
136
|
+
text: number
|
|
137
|
+
inner: number[]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface Stadium {
|
|
141
|
+
defaultStadiumId: number
|
|
142
|
+
name: string | null
|
|
143
|
+
backgroundType: BackgroundType | null
|
|
144
|
+
backgroundWidth: number | null
|
|
145
|
+
backgroundHeight: number | null
|
|
146
|
+
backgroundKickoffRadius: number | null
|
|
147
|
+
backgroundCornerRadius: number | null
|
|
148
|
+
backgroundGoalLine: number | null
|
|
149
|
+
backgroundColor: number | null
|
|
150
|
+
width: number | null
|
|
151
|
+
height: number | null
|
|
152
|
+
spawnDistance: number | null
|
|
153
|
+
playerPhysics: PlayerPhysics | null
|
|
154
|
+
maxViewWidth: number | null
|
|
155
|
+
cameraFollow: CameraFollow | null
|
|
156
|
+
canBeStored: boolean | null
|
|
157
|
+
fullKickoffReset: boolean | null
|
|
158
|
+
vertices: Vertex[]
|
|
159
|
+
segments: Segment[]
|
|
160
|
+
planes: Plane[]
|
|
161
|
+
goals: Goal[]
|
|
162
|
+
discs: Disc[]
|
|
163
|
+
joints: Joint[]
|
|
164
|
+
redSpawnPoints: Point[]
|
|
165
|
+
blueSpawnPoints: Point[]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface Player {
|
|
169
|
+
isAdmin: boolean
|
|
170
|
+
avatarNumber: number
|
|
171
|
+
avatar: string | null
|
|
172
|
+
headlessAvatar: string | null
|
|
173
|
+
sync: boolean
|
|
174
|
+
flag: string | null
|
|
175
|
+
metadata: number
|
|
176
|
+
name: string | null
|
|
177
|
+
input: number
|
|
178
|
+
id: number
|
|
179
|
+
isKicking: boolean
|
|
180
|
+
kickRateMaxTickCounter: number
|
|
181
|
+
kickRateMinTickCounter: number
|
|
182
|
+
teamId: number
|
|
183
|
+
discIndex: number
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface GameState {
|
|
187
|
+
discs: Disc[]
|
|
188
|
+
goalTickCounter: number
|
|
189
|
+
state: GamePlayState
|
|
190
|
+
redScore: number
|
|
191
|
+
blueScore: number
|
|
192
|
+
timeElapsed: number
|
|
193
|
+
pauseGameTickCounter: number
|
|
194
|
+
goalConcedingTeam: number
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface RoomState {
|
|
198
|
+
name: string | null
|
|
199
|
+
teamsLocked: boolean
|
|
200
|
+
scoreLimit: number
|
|
201
|
+
timeLimit: number
|
|
202
|
+
kickRateMax: number
|
|
203
|
+
kickRateRate: number
|
|
204
|
+
kickRateMin: number
|
|
205
|
+
stadium: Stadium
|
|
206
|
+
gameState: GameState | null
|
|
207
|
+
players: Player[]
|
|
208
|
+
redTeamColors: TeamColors
|
|
209
|
+
blueTeamColors: TeamColors
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface GoalMarker {
|
|
213
|
+
frameNo: number
|
|
214
|
+
teamId: number
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface SendAnnouncementEvent {
|
|
218
|
+
msg: string
|
|
219
|
+
color: number
|
|
220
|
+
style: number
|
|
221
|
+
sound: number
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface SendChatIndicatorEvent {
|
|
225
|
+
value: number
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface CheckConsistencyEvent {
|
|
229
|
+
data: number[]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface SendInputEvent {
|
|
233
|
+
input: number
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface SendChatEvent {
|
|
237
|
+
text: string
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface JoinRoomEvent {
|
|
241
|
+
playerId: number
|
|
242
|
+
name: string | null
|
|
243
|
+
flag: string | null
|
|
244
|
+
avatar: string | null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface KickBanPlayerEvent {
|
|
248
|
+
playerId: number
|
|
249
|
+
reason: string | null
|
|
250
|
+
ban: boolean
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export type StartGameEvent = Record<string, never>
|
|
254
|
+
export type StopGameEvent = Record<string, never>
|
|
255
|
+
|
|
256
|
+
export interface PauseResumeGameEvent {
|
|
257
|
+
paused: boolean
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface SetGamePlayLimitEvent {
|
|
261
|
+
limitType: number
|
|
262
|
+
newValue: number
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface SetStadiumEvent {
|
|
266
|
+
stadium: Stadium
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface SetPlayerTeamEvent {
|
|
270
|
+
playerId: number
|
|
271
|
+
teamId: number
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface SetTeamsLockEvent {
|
|
275
|
+
newValue: boolean
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface SetPlayerAdminEvent {
|
|
279
|
+
playerId: number
|
|
280
|
+
value: boolean
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export type AutoTeamsEvent = Record<string, never>
|
|
284
|
+
|
|
285
|
+
export interface SetPlayerSyncEvent {
|
|
286
|
+
value: boolean
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface PingEvent {
|
|
290
|
+
pings: number[]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface SetAvatarEvent {
|
|
294
|
+
value: string | null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface SetTeamColorsEvent {
|
|
298
|
+
teamId: number
|
|
299
|
+
colors: TeamColors
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface ReorderPlayersEvent {
|
|
303
|
+
moveToTop: boolean
|
|
304
|
+
playerIdList: number[]
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface SetKickRateLimitEvent {
|
|
308
|
+
min: number
|
|
309
|
+
rate: number
|
|
310
|
+
burst: number
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface SetHeadlessAvatarEvent {
|
|
314
|
+
value: string | null
|
|
315
|
+
playerId: number
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export type DiscFloatProperties = [
|
|
319
|
+
number | null,
|
|
320
|
+
number | null,
|
|
321
|
+
number | null,
|
|
322
|
+
number | null,
|
|
323
|
+
number | null,
|
|
324
|
+
number | null,
|
|
325
|
+
number | null,
|
|
326
|
+
number | null,
|
|
327
|
+
number | null,
|
|
328
|
+
number | null,
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
export type DiscIntegerProperties = [number | null, number | null, number | null]
|
|
332
|
+
|
|
333
|
+
export interface SetDiscPropertiesEvent {
|
|
334
|
+
id: number
|
|
335
|
+
isPlayer: boolean
|
|
336
|
+
flags: number
|
|
337
|
+
data1: DiscFloatProperties
|
|
338
|
+
data2: DiscIntegerProperties
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export interface CustomEvent {
|
|
342
|
+
eventType: number
|
|
343
|
+
data: unknown
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface BinaryCustomEvent {
|
|
347
|
+
eventType: number
|
|
348
|
+
data: number[]
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export interface SetPlayerIdentityEvent {
|
|
352
|
+
id: number
|
|
353
|
+
data: unknown
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface UnknownEvent {
|
|
357
|
+
eventType: number
|
|
358
|
+
rawPayload: number[]
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export type EventPayload =
|
|
362
|
+
| { kind: 'sendAnnouncement'; value: SendAnnouncementEvent }
|
|
363
|
+
| { kind: 'sendChatIndicator'; value: SendChatIndicatorEvent }
|
|
364
|
+
| { kind: 'checkConsistency'; value: CheckConsistencyEvent }
|
|
365
|
+
| { kind: 'sendInput'; value: SendInputEvent }
|
|
366
|
+
| { kind: 'sendChat'; value: SendChatEvent }
|
|
367
|
+
| { kind: 'joinRoom'; value: JoinRoomEvent }
|
|
368
|
+
| { kind: 'kickBanPlayer'; value: KickBanPlayerEvent }
|
|
369
|
+
| { kind: 'startGame'; value: StartGameEvent }
|
|
370
|
+
| { kind: 'stopGame'; value: StopGameEvent }
|
|
371
|
+
| { kind: 'pauseResumeGame'; value: PauseResumeGameEvent }
|
|
372
|
+
| { kind: 'setGamePlayLimit'; value: SetGamePlayLimitEvent }
|
|
373
|
+
| { kind: 'setStadium'; value: SetStadiumEvent }
|
|
374
|
+
| { kind: 'setPlayerTeam'; value: SetPlayerTeamEvent }
|
|
375
|
+
| { kind: 'setTeamsLock'; value: SetTeamsLockEvent }
|
|
376
|
+
| { kind: 'setPlayerAdmin'; value: SetPlayerAdminEvent }
|
|
377
|
+
| { kind: 'autoTeams'; value: AutoTeamsEvent }
|
|
378
|
+
| { kind: 'setPlayerSync'; value: SetPlayerSyncEvent }
|
|
379
|
+
| { kind: 'ping'; value: PingEvent }
|
|
380
|
+
| { kind: 'setAvatar'; value: SetAvatarEvent }
|
|
381
|
+
| { kind: 'setTeamColors'; value: SetTeamColorsEvent }
|
|
382
|
+
| { kind: 'reorderPlayers'; value: ReorderPlayersEvent }
|
|
383
|
+
| { kind: 'setKickRateLimit'; value: SetKickRateLimitEvent }
|
|
384
|
+
| { kind: 'setHeadlessAvatar'; value: SetHeadlessAvatarEvent }
|
|
385
|
+
| { kind: 'setDiscProperties'; value: SetDiscPropertiesEvent }
|
|
386
|
+
| { kind: 'customEvent'; value: CustomEvent }
|
|
387
|
+
| { kind: 'binaryCustomEvent'; value: BinaryCustomEvent }
|
|
388
|
+
| { kind: 'setPlayerIdentity'; value: SetPlayerIdentityEvent }
|
|
389
|
+
| { kind: 'unknown'; value: UnknownEvent }
|
|
390
|
+
|
|
391
|
+
export interface ReplayEvent {
|
|
392
|
+
frameNo: number
|
|
393
|
+
byId: number
|
|
394
|
+
payload: EventPayload
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface ReplayData {
|
|
398
|
+
roomData: RoomState
|
|
399
|
+
events: ReplayEvent[]
|
|
400
|
+
goalMarkers: GoalMarker[]
|
|
401
|
+
totalFrames: number
|
|
402
|
+
version: number
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export type DecodeErrorKind =
|
|
406
|
+
| 'invalidMagic'
|
|
407
|
+
| 'unexpectedEof'
|
|
408
|
+
| 'invalidVarInt'
|
|
409
|
+
| 'invalidUtf8'
|
|
410
|
+
| 'invalidJson'
|
|
411
|
+
| 'compression'
|
|
412
|
+
| 'incompleteCompression'
|
|
413
|
+
| 'trailingCompressedData'
|
|
414
|
+
| 'unsupportedReplayVersion'
|
|
415
|
+
| 'unsupportedEventType'
|
|
416
|
+
| 'unknownEventBoundaryUnsupported'
|
|
417
|
+
| 'integerOverflow'
|
|
418
|
+
| 'trailingBytes'
|
|
419
|
+
| 'validationFailed'
|
|
420
|
+
|
|
421
|
+
export type DecodeErrorDetails =
|
|
422
|
+
| { kind: 'invalidMagic'; found: number[] }
|
|
423
|
+
| { kind: 'unexpectedEof'; context: string }
|
|
424
|
+
| { kind: 'invalidVarInt'; context: string }
|
|
425
|
+
| { kind: 'invalidUtf8'; context: string; source: string }
|
|
426
|
+
| { kind: 'invalidJson'; context: string; source: string }
|
|
427
|
+
| { kind: 'compression'; context: string; source: string }
|
|
428
|
+
| { kind: 'incompleteCompression'; context: string }
|
|
429
|
+
| { kind: 'trailingCompressedData'; context: string }
|
|
430
|
+
| { kind: 'unsupportedReplayVersion'; version: number }
|
|
431
|
+
| { kind: 'unsupportedEventType'; eventType: number }
|
|
432
|
+
| { kind: 'unknownEventBoundaryUnsupported'; eventType: number }
|
|
433
|
+
| { kind: 'integerOverflow'; context: string }
|
|
434
|
+
| { kind: 'trailingBytes'; context: string; remaining: number }
|
|
435
|
+
| { kind: 'validationFailed'; report: ValidationReport }
|
|
436
|
+
|
|
437
|
+
export interface DecodeFailure {
|
|
438
|
+
kind: DecodeErrorKind
|
|
439
|
+
message: string
|
|
440
|
+
details: DecodeErrorDetails
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export type DecodeResult = { ok: true; data: ReplayData } | { ok: false; error: DecodeFailure }
|