@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 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 }