@juande_pl/rocketleague-stats-api-client 0.0.0-stage → 1.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 +204 -3
- package/dist/chunk-5ZC7WJPK.js +776 -0
- package/dist/chunk-5ZC7WJPK.js.map +1 -0
- package/dist/chunk-IUPCJADJ.js +182 -0
- package/dist/chunk-IUPCJADJ.js.map +1 -0
- package/dist/config-3VSG3YBY.js +23 -0
- package/dist/config-3VSG3YBY.js.map +1 -0
- package/dist/index.cjs +1082 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +961 -0
- package/dist/index.d.ts +961 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +1 -0
- package/dist/node.cjs +1103 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +174 -0
- package/dist/node.js.map +1 -0
- package/package.json +78 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types/common.ts","../src/types/events.ts","../src/types/commands.ts","../src/guards/primitives.ts","../src/guards/events.ts","../src/guards/commands.ts","../src/parse.ts","../src/sort.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * Shared structures used across Stats API events.\n *\n * Field visibility markers used in the docs below (mirroring the official documentation):\n * - `CONDITIONAL`: only present when relevant.\n * - `SPECTATOR`: only sent while spectating.\n * - `REPLAY`: only relevant during replays.\n *\n * @see https://www.rocketleague.com/developer/stats-api\n */\n\n/** Team index. `0` = Blue, `1` = Orange. */\nexport type TeamNum = number;\n\n/** Well-known team indexes. */\nexport const Team = {\n\tBlue: 0,\n\tOrange: 1,\n} as const;\n\n/** Value of `Game.Ball.TeamNum` when the ball has not been touched yet. */\nexport const BALL_UNTOUCHED_TEAM_NUM = 255;\n\n/** A 3D world position in Unreal Units. */\nexport interface Vector {\n\tX: number;\n\tY: number;\n\tZ: number;\n}\n\n/** Minimal reference to a player, used by most events. */\nexport interface PlayerRef {\n\t/** Display name. */\n\tName: string;\n\t/** Spectator shortcut number. */\n\tShortcut: number;\n\t/** Team index (0 = Blue, 1 = Orange). */\n\tTeamNum: TeamNum;\n}\n\n/** The last touch of the ball before an event (goal, crossbar hit...). */\nexport interface BallLastTouch {\n\t/** The player who made the last touch. */\n\tPlayer: PlayerRef;\n\t/** Speed of the ball resulting from this touch. */\n\tSpeed: number;\n}\n\n/**\n * Platform identifier in the format `Platform|Uid|Splitscreen`\n * (e.g. `\"Steam|123|0\"`, `\"Epic|456|0\"`).\n */\nexport type PrimaryId = string;\n\n/** Full player state, sent in every `UpdateState` tick. */\nexport interface Player extends PlayerRef {\n\t/** Platform identifier in the format `Platform|Uid|Splitscreen`. */\n\tPrimaryId: PrimaryId;\n\t/** Total match score. */\n\tScore: number;\n\t/** Goals scored this match. */\n\tGoals: number;\n\t/** Shot attempts this match. */\n\tShots: number;\n\t/** Assists earned this match. */\n\tAssists: number;\n\t/** Saves made this match. */\n\tSaves: number;\n\t/** Total ball touches. */\n\tTouches: number;\n\t/** Touches by the car body (not ball). */\n\tCarTouches: number;\n\t/** Demolitions inflicted. */\n\tDemos: number;\n\t/**\n\t * Asset names of the equipped loadout for the player's current team, sorted by product slot.\n\t * Typed as optional because older game builds did not send it.\n\t */\n\tLoadout?: string[];\n\t/** `SPECTATOR` True if the player currently has a vehicle. */\n\tbHasCar?: boolean;\n\t/** `SPECTATOR` Vehicle speed in Unreal Units/second. */\n\tSpeed?: number;\n\t/** `SPECTATOR` Boost amount 0–100. */\n\tBoost?: number;\n\t/** `SPECTATOR` True if the player is currently boosting. */\n\tbBoosting?: boolean;\n\t/** `SPECTATOR` True if at least 3 wheels are touching the world. */\n\tbOnGround?: boolean;\n\t/** `SPECTATOR` True if the vehicle is on a wall. */\n\tbOnWall?: boolean;\n\t/** `SPECTATOR` True if the player is holding handbrake. */\n\tbPowersliding?: boolean;\n\t/** `SPECTATOR` True if the vehicle is currently destroyed. */\n\tbDemolished?: boolean;\n\t/** `SPECTATOR` True if the vehicle is at supersonic speed. */\n\tbSupersonic?: boolean;\n\t/**\n\t * `SPECTATOR` Class name of the player's currently available Rumble pickup\n\t * (e.g. `\"SpecialPickup_GrapplingHook_TA\"`). Omitted when not playing with rumble powerups.\n\t */\n\tPickupClass?: string;\n\t/** `CONDITIONAL` The player who demolished this player. Present only when demolished. */\n\tAttacker?: PlayerRef;\n}\n\n/** A team entry inside `UpdateState.Game.Teams`. */\nexport interface GameTeam {\n\t/** Team name. */\n\tName: string;\n\t/** Team index. */\n\tTeamNum: TeamNum;\n\t/** Team goal count. */\n\tScore: number;\n\t/** Hex color code (no `#`) for the team's primary color, e.g. `\"0000FF\"`. */\n\tColorPrimary: string;\n\t/** Hex color code (no `#`) for the team's secondary color. */\n\tColorSecondary: string;\n}\n\n/** Current ball state inside `UpdateState.Game.Ball`. */\nexport interface GameBall {\n\t/** Current ball speed in Unreal Units/second. */\n\tSpeed: number;\n\t/** Index of the last team to touch the ball. `255` if the ball has not been touched. */\n\tTeamNum: TeamNum;\n}\n\n/** Match metadata, sent in every `UpdateState` tick. */\nexport interface Game {\n\t/** One entry per team, ordered by TeamNum. */\n\tTeams: GameTeam[];\n\t/**\n\t * Playlist Id of the current match.\n\t * Typed as optional because older game builds did not send it.\n\t */\n\tPlaylistId?: number;\n\t/** Seconds remaining in the match. */\n\tTimeSeconds: number;\n\t/** True if the match is in overtime. */\n\tbOvertime: boolean;\n\t/** Current ball state. */\n\tBall: GameBall;\n\t/** True if a goal replay or history replay is active. */\n\tbReplay: boolean;\n\t/** True if a team has won. */\n\tbHasWinner: boolean;\n\t/** Name of the winning team. Empty string if no winner yet. */\n\tWinner: string;\n\t/** Asset name of the current map (e.g. `\"Stadium_P\"`). */\n\tArena: string;\n\t/** True if the client is currently viewing a specific vehicle. */\n\tbHasTarget: boolean;\n\t/**\n\t * `CONDITIONAL` Player currently being viewed. Members are an empty string or `0`\n\t * if the player does not have a spectator target.\n\t */\n\tTarget?: PlayerRef;\n\t/** `CONDITIONAL` `REPLAY` Current frame number if a replay is active. */\n\tFrame?: number;\n\t/** `CONDITIONAL` `REPLAY` Seconds elapsed since game start if a replay is active. */\n\tElapsed?: number;\n}\n","import type { BallLastTouch, Game, Player, PlayerRef, PrimaryId, TeamNum, Vector } from './common.js';\n\n/** Every event name the Stats API can emit. */\nexport const EVENT_NAMES = [\n\t'UpdateState',\n\t'BallHit',\n\t'BoostPickup',\n\t'ClockUpdatedSeconds',\n\t'CountdownBegin',\n\t'CrossbarHit',\n\t'GoalReplayEnd',\n\t'GoalReplayStart',\n\t'GoalReplayWillEnd',\n\t'GoalScored',\n\t'MatchCreated',\n\t'MatchDestroyed',\n\t'MatchEnded',\n\t'MatchInitialized',\n\t'MatchPaused',\n\t'MatchUnpaused',\n\t'PlayerJoined',\n\t'PlayerLeft',\n\t'PodiumStart',\n\t'ReplayCreated',\n\t'RoundStarted',\n\t'StatfeedEvent',\n] as const;\n\n/** Union of every event name the Stats API can emit. */\nexport type EventName = (typeof EVENT_NAMES)[number];\n\n/** Fields present in every event payload. */\nexport interface BaseEventData {\n\t/** Match identifier. Only set for online or LAN matches. */\n\tMatchGuid?: string;\n}\n\n/** Sent X times per second, based on the player's `PacketSendRate` setting. */\nexport interface UpdateStateData extends BaseEventData {\n\t/** One entry per player in the match. */\n\tPlayers: Player[];\n\t/** Match metadata. */\n\tGame: Game;\n}\n\n/** Sent one frame after the ball is hit. */\nexport interface BallHitData extends BaseEventData {\n\t/** Players that hit the ball that frame. */\n\tPlayers: PlayerRef[];\n\t/** Ball state at the moment of the hit. */\n\tBall: {\n\t\t/** Ball speed before the hit (Unreal Units/second). */\n\t\tPreHitSpeed: number;\n\t\t/** Ball speed after the hit (Unreal Units/second). */\n\t\tPostHitSpeed: number;\n\t\t/** World position of the ball at impact. */\n\t\tLocation: Vector;\n\t};\n}\n\n/** Known boost pickup classes. */\nexport type BoostType = 'BoostType_Pad' | 'BoostType_Pill';\n\n/** `SPECTATOR` Sent when a vehicle collects a boost pad or pill. */\nexport interface BoostPickupData extends BaseEventData {\n\t/** The player who collected the boost. */\n\tPlayer: PlayerRef;\n\t/** World location of the pickup. */\n\tLocation: Vector;\n\t/** Amount of boost granted by the pickup. */\n\tBoostAmount: number;\n\t/** Pickup class: `BoostType_Pad` (small pad), `BoostType_Pill` (full/big boost). */\n\tBoostType: BoostType;\n\t/** True if the pickup occurred during replay playback. */\n\tbReplay: boolean;\n}\n\n/** Sent when the in-game clock has changed. */\nexport interface ClockUpdatedSecondsData extends BaseEventData {\n\t/** Seconds remaining in the match. */\n\tTimeSeconds: number;\n\t/** True if the game is in overtime. */\n\tbOvertime: boolean;\n}\n\n/** Sent at the start of each round when the countdown starts. */\nexport interface CountdownBeginData extends BaseEventData {}\n\n/** Sent when the ball hits a crossbar. */\nexport interface CrossbarHitData extends BaseEventData {\n\t/** World position of the ball when the impact occurred. */\n\tBallLocation: Vector;\n\t/** Ball speed on impact. */\n\tBallSpeed: number;\n\t/** Impact force of the ball relative to the crossbar normal. */\n\tImpactForce: number;\n\t/** The last touch of the ball before the crossbar hit. */\n\tBallLastTouch: BallLastTouch;\n}\n\n/** Sent when a goal replay ends. */\nexport interface GoalReplayEndData extends BaseEventData {}\n\n/** Sent when a goal replay starts. */\nexport interface GoalReplayStartData extends BaseEventData {}\n\n/** Sent when the ball explodes during a goal replay. Does not fire if the replay is skipped. */\nexport interface GoalReplayWillEndData extends BaseEventData {}\n\n/** Sent when a goal is scored. */\nexport interface GoalScoredData extends BaseEventData {\n\t/** Speed of the ball (Unreal Units/second) when it crossed the goal line. */\n\tGoalSpeed: number;\n\t/** Length of the previous round in seconds (not the match clock). */\n\tGoalTime: number;\n\t/** World position of the ball when the goal was scored. */\n\tImpactLocation: Vector;\n\t/** The player who scored the goal. */\n\tScorer: PlayerRef;\n\t/** `CONDITIONAL` Present only when an assist was recorded. */\n\tAssister?: PlayerRef;\n\t/** The last touch of the ball before the goal. */\n\tBallLastTouch: BallLastTouch;\n}\n\n/** Sent when all teams are created and replicated. */\nexport interface MatchCreatedData extends BaseEventData {}\n\n/** Sent when leaving the game. */\nexport interface MatchDestroyedData extends BaseEventData {}\n\n/** Sent when the match ends and a winner is chosen. */\nexport interface MatchEndedData extends BaseEventData {\n\t/** Team index of the winning team. */\n\tWinnerTeamNum: TeamNum;\n}\n\n/** Sent when the first countdown starts. */\nexport interface MatchInitializedData extends BaseEventData {}\n\n/** Sent when the game is paused by a match admin. */\nexport interface MatchPausedData extends BaseEventData {}\n\n/** Sent when the game is unpaused by a match admin. */\nexport interface MatchUnpausedData extends BaseEventData {}\n\n/** Sent when a player is added to the current match. */\nexport interface PlayerJoinedData extends BaseEventData {\n\t/** Display name of the player who joined. */\n\tPlayerName: string;\n\t/** Platform identifier in the format `Platform|Uid|Splitscreen`. */\n\tPrimaryId: PrimaryId;\n}\n\n/** Sent when a player is removed from the current match. */\nexport interface PlayerLeftData extends BaseEventData {\n\t/** Display name of the player who left. */\n\tPlayerName: string;\n\t/** Platform identifier in the format `Platform|Uid|Splitscreen`. */\n\tPrimaryId: PrimaryId;\n}\n\n/** Sent when the game enters the podium state after the match ends. */\nexport interface PodiumStartData extends BaseEventData {}\n\n/**\n * Sent when a replay is initialized. Does not pertain to goal replays,\n * only replays loaded via the Match History menu.\n */\nexport interface ReplayCreatedData extends BaseEventData {\n\t/** File name of the replay. */\n\tFileName: string;\n\t/** Timestamp the replay was created (e.g. `\"2026-06-05 18:42:13\"`). */\n\tDate: string;\n}\n\n/** Sent when the game enters the active state (after the countdown finishes). */\nexport interface RoundStartedData extends BaseEventData {}\n\n/**\n * Known `StatfeedEvent.EventName` values. The type stays open (`string & {}`)\n * so new stats added by the game don't break consumers.\n */\nexport type StatEventName =\n\t| 'Demolish'\n\t| 'Demolition'\n\t| 'Goal'\n\t| 'Win'\n\t| 'MVP'\n\t| 'AerialGoal'\n\t| 'BackwardsGoal'\n\t| 'BicycleGoal'\n\t| 'LongGoal'\n\t| 'TurtleGoal'\n\t| 'PoolShot'\n\t| 'OvertimeGoal'\n\t| 'HatTrick'\n\t| 'Assist'\n\t| 'Playmaker'\n\t| 'Save'\n\t| 'EpicSave'\n\t| 'Savior'\n\t| 'Shot'\n\t| 'Center'\n\t| 'Clear'\n\t| 'FirstTouch'\n\t| 'BreakoutDamage'\n\t| 'BreakoutDamageLarge'\n\t| 'LowFive'\n\t| 'HighFive'\n\t| 'HoopsSwishGoal'\n\t| 'BicycleHit'\n\t| 'OwnGoal'\n\t| 'FlipReset'\n\t| (string & {});\n\n/** Sent when someone earns a stat. */\nexport interface StatfeedEventData extends BaseEventData {\n\t/** Asset name of the StatEvent (e.g. `\"Demolish\"`, `\"Save\"`). Prefer this over `Type` for logic. */\n\tEventName: StatEventName;\n\t/** Localized display label for the stat (e.g. `\"Demolition\"`). */\n\tType: string;\n\t/** Player who earned the stat. */\n\tMainTarget: PlayerRef;\n\t/** `CONDITIONAL` Player involved in the stat (e.g. the demolished player). */\n\tSecondaryTarget?: PlayerRef;\n}\n\n/** Maps every event name to its payload type. */\nexport interface EventDataMap {\n\tUpdateState: UpdateStateData;\n\tBallHit: BallHitData;\n\tBoostPickup: BoostPickupData;\n\tClockUpdatedSeconds: ClockUpdatedSecondsData;\n\tCountdownBegin: CountdownBeginData;\n\tCrossbarHit: CrossbarHitData;\n\tGoalReplayEnd: GoalReplayEndData;\n\tGoalReplayStart: GoalReplayStartData;\n\tGoalReplayWillEnd: GoalReplayWillEndData;\n\tGoalScored: GoalScoredData;\n\tMatchCreated: MatchCreatedData;\n\tMatchDestroyed: MatchDestroyedData;\n\tMatchEnded: MatchEndedData;\n\tMatchInitialized: MatchInitializedData;\n\tMatchPaused: MatchPausedData;\n\tMatchUnpaused: MatchUnpausedData;\n\tPlayerJoined: PlayerJoinedData;\n\tPlayerLeft: PlayerLeftData;\n\tPodiumStart: PodiumStartData;\n\tReplayCreated: ReplayCreatedData;\n\tRoundStarted: RoundStartedData;\n\tStatfeedEvent: StatfeedEventData;\n}\n\n/** A decoded event message with its name and typed payload. */\nexport interface StatsApiEventOf<E extends EventName> {\n\tEvent: E;\n\tData: EventDataMap[E];\n}\n\n/**\n * Discriminated union of every decoded event message.\n * Narrow it by checking `event.Event`:\n *\n * ```ts\n * if (event.Event === 'GoalScored') event.Data.Scorer.Name;\n * ```\n */\nexport type StatsApiEvent = { [E in EventName]: StatsApiEventOf<E> }[EventName];\n\n/**\n * The raw envelope as it arrives over the socket. The game encodes `Data`\n * as a JSON string; some tools forward it as an object, so both are accepted.\n */\nexport interface RawEventEnvelope {\n\tEvent: string;\n\tData: string | Record<string, unknown>;\n}\n","/** Every command name the Stats API accepts. */\nexport const COMMAND_NAMES = [\n\t'ChangePOV',\n\t'LoadReplay',\n\t'SeekReplay',\n\t'SetGameSpeed',\n\t'SetHUDVisibility',\n\t'SetMatchPaused',\n] as const;\n\n/** Union of every command name the Stats API accepts. */\nexport type CommandName = (typeof COMMAND_NAMES)[number];\n\n/** Accepted camera perspectives for `ChangePOV`. */\nexport const PERSPECTIVES = [\n\t/** Freely move around the map. */\n\t'Fly',\n\t/** Follow the position of a player or ball. */\n\t'SoftAttach',\n\t/** Follow the position and rotation of a player. */\n\t'HardAttach',\n\t/** View from a player's perspective. Applied automatically when Focus targets a player and no Perspective is given. */\n\t'PlayerView',\n\t/** Automatically spectates. */\n\t'AutoCam',\n\t/** Automatically switches between player views. */\n\t'Camera_Director',\n] as const;\n\n/** Camera mode for `ChangePOV`. */\nexport type Perspective = (typeof PERSPECTIVES)[number];\n\n/**\n * What to look at for `ChangePOV`: `\"Ball\"`, or a player's spectator shortcut\n * as an all-digit string (e.g. `\"1\"`). Any other value is rejected by the game.\n */\nexport type Focus = 'Ball' | `${number}`;\n\n/**\n * `SPECTATOR` `REPLAY` Changes the spectator/replay viewpoint.\n * At least one of `Focus` or `Perspective` must be supplied; both may be combined.\n */\nexport type ChangePOVData =\n\t| { Focus: Focus; Perspective?: Perspective }\n\t| { Focus?: Focus; Perspective: Perspective };\n\n/**\n * Loads and begins playback of a replay by file name or by file path.\n * At least one of `FileName` or `Path` must be supplied. `FileName` takes precedence.\n */\nexport type LoadReplayData =\n\t| { FileName: string; Path?: string }\n\t| { FileName?: string; Path: string };\n\n/**\n * `REPLAY` Jumps replay playback to a target frame or time.\n * At least one of `Frame` or `TimeSeconds` must be supplied. `Frame` takes precedence.\n */\nexport type SeekReplayData =\n\t| { Frame: number; TimeSeconds?: number }\n\t| { Frame?: number; TimeSeconds: number };\n\n/** `REPLAY` Sets replay playback speed. */\nexport interface SetGameSpeedData {\n\t/** Playback multiplier. 1.0 = normal, 0.5 = half speed, 2.0 = double. Must be ≥ 0. */\n\tSpeed: number;\n}\n\n/** Shows or hides the in-game HUD. */\nexport interface SetHUDVisibilityData {\n\t/** `true` shows the HUD; `false` hides it. */\n\tbVisible: boolean;\n}\n\n/** Pauses or unpauses the current match or replay. */\nexport interface SetMatchPausedData {\n\t/** `true` pauses; `false` resumes. */\n\tbPaused: boolean;\n}\n\n/** Maps every command name to its payload type. */\nexport interface CommandDataMap {\n\tChangePOV: ChangePOVData;\n\tLoadReplay: LoadReplayData;\n\tSeekReplay: SeekReplayData;\n\tSetGameSpeed: SetGameSpeedData;\n\tSetHUDVisibility: SetHUDVisibilityData;\n\tSetMatchPaused: SetMatchPausedData;\n}\n\n/** A command envelope with its name and typed payload. */\nexport interface StatsApiCommandOf<C extends CommandName> {\n\tCommand: C;\n\tData: CommandDataMap[C];\n}\n\n/** Discriminated union of every command envelope. */\nexport type StatsApiCommand = { [C in CommandName]: StatsApiCommandOf<C> }[CommandName];\n","/**\n * Small, dependency-free building blocks for runtime type guards.\n * They are exported so consumers can compose their own guards too.\n */\n\n/** A runtime type guard. */\nexport type Guard<T> = (value: unknown) => value is T;\n\n/** Extracts the guarded type from a {@link Guard}. */\nexport type GuardedType<G> = G extends Guard<infer T> ? T : never;\n\n/** Checks that a value is a non-null, non-array object. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport const isString: Guard<string> = (value): value is string => typeof value === 'string';\n\n/** Checks for a finite number (rejects `NaN` and `Infinity`). */\nexport const isNumber: Guard<number> = (value): value is number =>\n\ttypeof value === 'number' && Number.isFinite(value);\n\n/** Checks for a finite integer. */\nexport const isInteger: Guard<number> = (value): value is number => Number.isInteger(value);\n\nexport const isBoolean: Guard<boolean> = (value): value is boolean => typeof value === 'boolean';\n\n/** Accepts `undefined` or a value matching `guard`. */\nexport function optional<T>(guard: Guard<T>): Guard<T | undefined> {\n\treturn (value): value is T | undefined => value === undefined || guard(value);\n}\n\n/** Checks for an array whose items all match `guard`. */\nexport function arrayOf<T>(guard: Guard<T>): Guard<T[]> {\n\treturn (value): value is T[] => Array.isArray(value) && value.every((item) => guard(item));\n}\n\n/** Checks that a value is one of the given literals. */\nexport function oneOf<const T extends readonly (string | number | boolean)[]>(values: T): Guard<T[number]> {\n\tconst set = new Set<unknown>(values);\n\treturn (value): value is T[number] => set.has(value);\n}\n\n/** Specification for {@link shape}: one guard per property of `T`, optional ones included. */\nexport type ShapeSpec<T> = { [K in keyof Required<T>]: Guard<T[K]> };\n\n/**\n * Builds a guard for an object type from one guard per property.\n * Extra properties are allowed so new fields added by the game don't break validation.\n * The spec is checked against `T` at compile time, so a missing property is a type error.\n */\nexport function shape<T extends object>(spec: ShapeSpec<T>): Guard<T> {\n\tconst entries = Object.entries(spec) as [string, Guard<unknown>][];\n\treturn (value): value is T => {\n\t\tif (!isRecord(value)) return false;\n\t\tfor (const [key, guard] of entries) {\n\t\t\tif (!guard(value[key])) return false;\n\t\t}\n\t\treturn true;\n\t};\n}\n\n/** Combines guards with logical AND. */\nexport function allOf<T>(guard: Guard<T>, ...checks: ((value: T) => boolean)[]): Guard<T> {\n\treturn (value): value is T => guard(value) && checks.every((check) => check(value));\n}\n","import type { BallLastTouch, Game, GameBall, GameTeam, Player, PlayerRef, Vector } from '../types/common.js';\nimport {\n\tEVENT_NAMES,\n\ttype BallHitData,\n\ttype BaseEventData,\n\ttype BoostPickupData,\n\ttype ClockUpdatedSecondsData,\n\ttype CrossbarHitData,\n\ttype EventDataMap,\n\ttype EventName,\n\ttype GoalScoredData,\n\ttype MatchEndedData,\n\ttype PlayerJoinedData,\n\ttype PlayerLeftData,\n\ttype RawEventEnvelope,\n\ttype ReplayCreatedData,\n\ttype StatfeedEventData,\n\ttype StatsApiEvent,\n\ttype StatsApiEventOf,\n\ttype UpdateStateData,\n} from '../types/events.js';\nimport { arrayOf, isBoolean, isNumber, isRecord, isString, oneOf, optional, shape, type Guard } from './primitives.js';\n\n// ---------------------------------------------------------------------------\n// Shared structures\n// ---------------------------------------------------------------------------\n\nexport const isVector: Guard<Vector> = shape<Vector>({ X: isNumber, Y: isNumber, Z: isNumber });\n\nexport const isPlayerRef: Guard<PlayerRef> = shape<PlayerRef>({\n\tName: isString,\n\tShortcut: isNumber,\n\tTeamNum: isNumber,\n});\n\nexport const isBallLastTouch: Guard<BallLastTouch> = shape<BallLastTouch>({\n\tPlayer: isPlayerRef,\n\tSpeed: isNumber,\n});\n\nexport const isPlayer: Guard<Player> = shape<Player>({\n\tName: isString,\n\tShortcut: isNumber,\n\tTeamNum: isNumber,\n\tPrimaryId: isString,\n\tScore: isNumber,\n\tGoals: isNumber,\n\tShots: isNumber,\n\tAssists: isNumber,\n\tSaves: isNumber,\n\tTouches: isNumber,\n\tCarTouches: isNumber,\n\tDemos: isNumber,\n\tLoadout: optional(arrayOf(isString)),\n\tbHasCar: optional(isBoolean),\n\tSpeed: optional(isNumber),\n\tBoost: optional(isNumber),\n\tbBoosting: optional(isBoolean),\n\tbOnGround: optional(isBoolean),\n\tbOnWall: optional(isBoolean),\n\tbPowersliding: optional(isBoolean),\n\tbDemolished: optional(isBoolean),\n\tbSupersonic: optional(isBoolean),\n\tPickupClass: optional(isString),\n\tAttacker: optional(isPlayerRef),\n});\n\nexport const isGameTeam: Guard<GameTeam> = shape<GameTeam>({\n\tName: isString,\n\tTeamNum: isNumber,\n\tScore: isNumber,\n\tColorPrimary: isString,\n\tColorSecondary: isString,\n});\n\nexport const isGameBall: Guard<GameBall> = shape<GameBall>({ Speed: isNumber, TeamNum: isNumber });\n\nexport const isGame: Guard<Game> = shape<Game>({\n\tTeams: arrayOf(isGameTeam),\n\tPlaylistId: optional(isNumber),\n\tTimeSeconds: isNumber,\n\tbOvertime: isBoolean,\n\tBall: isGameBall,\n\tbReplay: isBoolean,\n\tbHasWinner: isBoolean,\n\tWinner: isString,\n\tArena: isString,\n\tbHasTarget: isBoolean,\n\tTarget: optional(isPlayerRef),\n\tFrame: optional(isNumber),\n\tElapsed: optional(isNumber),\n});\n\n// ---------------------------------------------------------------------------\n// Event payloads\n// ---------------------------------------------------------------------------\n\nconst matchGuid = optional(isString);\n\n/** Guard for events whose payload only carries `MatchGuid`. */\nconst isBaseEventData: Guard<BaseEventData> = shape<BaseEventData>({ MatchGuid: matchGuid });\n\nexport const isUpdateStateData: Guard<UpdateStateData> = shape<UpdateStateData>({\n\tMatchGuid: matchGuid,\n\tPlayers: arrayOf(isPlayer),\n\tGame: isGame,\n});\n\nexport const isBallHitData: Guard<BallHitData> = shape<BallHitData>({\n\tMatchGuid: matchGuid,\n\tPlayers: arrayOf(isPlayerRef),\n\tBall: shape<BallHitData['Ball']>({\n\t\tPreHitSpeed: isNumber,\n\t\tPostHitSpeed: isNumber,\n\t\tLocation: isVector,\n\t}),\n});\n\nexport const isBoostPickupData: Guard<BoostPickupData> = shape<BoostPickupData>({\n\tMatchGuid: matchGuid,\n\tPlayer: isPlayerRef,\n\tLocation: isVector,\n\tBoostAmount: isNumber,\n\tBoostType: oneOf(['BoostType_Pad', 'BoostType_Pill']),\n\tbReplay: isBoolean,\n});\n\nexport const isClockUpdatedSecondsData: Guard<ClockUpdatedSecondsData> = shape<ClockUpdatedSecondsData>({\n\tMatchGuid: matchGuid,\n\tTimeSeconds: isNumber,\n\tbOvertime: isBoolean,\n});\n\nexport const isCrossbarHitData: Guard<CrossbarHitData> = shape<CrossbarHitData>({\n\tMatchGuid: matchGuid,\n\tBallLocation: isVector,\n\tBallSpeed: isNumber,\n\tImpactForce: isNumber,\n\tBallLastTouch: isBallLastTouch,\n});\n\nexport const isGoalScoredData: Guard<GoalScoredData> = shape<GoalScoredData>({\n\tMatchGuid: matchGuid,\n\tGoalSpeed: isNumber,\n\tGoalTime: isNumber,\n\tImpactLocation: isVector,\n\tScorer: isPlayerRef,\n\tAssister: optional(isPlayerRef),\n\tBallLastTouch: isBallLastTouch,\n});\n\nexport const isMatchEndedData: Guard<MatchEndedData> = shape<MatchEndedData>({\n\tMatchGuid: matchGuid,\n\tWinnerTeamNum: isNumber,\n});\n\nexport const isPlayerJoinedData: Guard<PlayerJoinedData> = shape<PlayerJoinedData>({\n\tMatchGuid: matchGuid,\n\tPlayerName: isString,\n\tPrimaryId: isString,\n});\n\nexport const isPlayerLeftData: Guard<PlayerLeftData> = shape<PlayerLeftData>({\n\tMatchGuid: matchGuid,\n\tPlayerName: isString,\n\tPrimaryId: isString,\n});\n\nexport const isReplayCreatedData: Guard<ReplayCreatedData> = shape<ReplayCreatedData>({\n\tMatchGuid: matchGuid,\n\tFileName: isString,\n\tDate: isString,\n});\n\nexport const isStatfeedEventData: Guard<StatfeedEventData> = shape<StatfeedEventData>({\n\tMatchGuid: matchGuid,\n\tEventName: isString,\n\tType: isString,\n\tMainTarget: isPlayerRef,\n\tSecondaryTarget: optional(isPlayerRef),\n});\n\nexport const isCountdownBeginData: Guard<EventDataMap['CountdownBegin']> = isBaseEventData;\nexport const isGoalReplayEndData: Guard<EventDataMap['GoalReplayEnd']> = isBaseEventData;\nexport const isGoalReplayStartData: Guard<EventDataMap['GoalReplayStart']> = isBaseEventData;\nexport const isGoalReplayWillEndData: Guard<EventDataMap['GoalReplayWillEnd']> = isBaseEventData;\nexport const isMatchCreatedData: Guard<EventDataMap['MatchCreated']> = isBaseEventData;\nexport const isMatchDestroyedData: Guard<EventDataMap['MatchDestroyed']> = isBaseEventData;\nexport const isMatchInitializedData: Guard<EventDataMap['MatchInitialized']> = isBaseEventData;\nexport const isMatchPausedData: Guard<EventDataMap['MatchPaused']> = isBaseEventData;\nexport const isMatchUnpausedData: Guard<EventDataMap['MatchUnpaused']> = isBaseEventData;\nexport const isPodiumStartData: Guard<EventDataMap['PodiumStart']> = isBaseEventData;\nexport const isRoundStartedData: Guard<EventDataMap['RoundStarted']> = isBaseEventData;\n\n/** Payload guard for every event, keyed by event name. */\nexport const eventDataGuards: { readonly [E in EventName]: Guard<EventDataMap[E]> } = {\n\tUpdateState: isUpdateStateData,\n\tBallHit: isBallHitData,\n\tBoostPickup: isBoostPickupData,\n\tClockUpdatedSeconds: isClockUpdatedSecondsData,\n\tCountdownBegin: isCountdownBeginData,\n\tCrossbarHit: isCrossbarHitData,\n\tGoalReplayEnd: isGoalReplayEndData,\n\tGoalReplayStart: isGoalReplayStartData,\n\tGoalReplayWillEnd: isGoalReplayWillEndData,\n\tGoalScored: isGoalScoredData,\n\tMatchCreated: isMatchCreatedData,\n\tMatchDestroyed: isMatchDestroyedData,\n\tMatchEnded: isMatchEndedData,\n\tMatchInitialized: isMatchInitializedData,\n\tMatchPaused: isMatchPausedData,\n\tMatchUnpaused: isMatchUnpausedData,\n\tPlayerJoined: isPlayerJoinedData,\n\tPlayerLeft: isPlayerLeftData,\n\tPodiumStart: isPodiumStartData,\n\tReplayCreated: isReplayCreatedData,\n\tRoundStarted: isRoundStartedData,\n\tStatfeedEvent: isStatfeedEventData,\n};\n\n// ---------------------------------------------------------------------------\n// Envelopes\n// ---------------------------------------------------------------------------\n\n/** Checks that a string is a known event name. */\nexport const isEventName: Guard<EventName> = oneOf(EVENT_NAMES);\n\n/** Checks that `data` is a valid payload for the event `name`. */\nexport function isEventData<E extends EventName>(name: E, data: unknown): data is EventDataMap[E] {\n\tconst guard: Guard<EventDataMap[E]> = eventDataGuards[name];\n\treturn guard(data);\n}\n\n/** Checks the raw envelope shape (`Event` string + `Data` string or object), without validating the payload. */\nexport function isRawEventEnvelope(value: unknown): value is RawEventEnvelope {\n\treturn isRecord(value) && isString(value.Event) && (isString(value.Data) || isRecord(value.Data));\n}\n\n/**\n * Checks that a value is a fully valid, decoded event message (with `Data` already parsed),\n * optionally restricted to a single event name.\n *\n * ```ts\n * if (isStatsApiEvent(msg, 'GoalScored')) msg.Data.Scorer.Name;\n * ```\n */\nexport function isStatsApiEvent(value: unknown): value is StatsApiEvent;\nexport function isStatsApiEvent<E extends EventName>(value: unknown, name: E): value is StatsApiEventOf<E>;\nexport function isStatsApiEvent(value: unknown, name?: EventName): boolean {\n\tif (!isRecord(value) || !isEventName(value.Event)) return false;\n\tif (name !== undefined && value.Event !== name) return false;\n\treturn isEventData(value.Event, value.Data);\n}\n","import {\n\tCOMMAND_NAMES,\n\tPERSPECTIVES,\n\ttype ChangePOVData,\n\ttype CommandDataMap,\n\ttype CommandName,\n\ttype Focus,\n\ttype LoadReplayData,\n\ttype Perspective,\n\ttype SeekReplayData,\n\ttype SetGameSpeedData,\n\ttype SetHUDVisibilityData,\n\ttype SetMatchPausedData,\n\ttype StatsApiCommand,\n\ttype StatsApiCommandOf,\n} from '../types/commands.js';\nimport { isBoolean, isInteger, isNumber, isRecord, isString, oneOf, optional, type Guard } from './primitives.js';\n\n/** Checks for `\"Ball\"` or an all-digit spectator shortcut string. */\nexport const isFocus: Guard<Focus> = (value): value is Focus =>\n\tvalue === 'Ball' || (isString(value) && /^\\d+$/.test(value));\n\nexport const isPerspective: Guard<Perspective> = oneOf(PERSPECTIVES);\n\n/** Checks that a key is present (not `undefined`) and matches `check`. */\nfunction has(record: Record<string, unknown>, key: string, check: (value: unknown) => boolean): boolean {\n\treturn record[key] !== undefined && check(record[key]);\n}\n\nexport const isChangePOVData: Guard<ChangePOVData> = (value): value is ChangePOVData =>\n\tisRecord(value) &&\n\toptional(isFocus)(value.Focus) &&\n\toptional(isPerspective)(value.Perspective) &&\n\t(value.Focus !== undefined || value.Perspective !== undefined);\n\n/** `FileName` / `Path` must be strings; at least one must be non-empty. */\nexport const isLoadReplayData: Guard<LoadReplayData> = (value): value is LoadReplayData =>\n\tisRecord(value) &&\n\toptional(isString)(value.FileName) &&\n\toptional(isString)(value.Path) &&\n\t(has(value, 'FileName', (v) => v !== '') || has(value, 'Path', (v) => v !== ''));\n\n/** `Frame` must be a non-negative integer, `TimeSeconds` a non-negative number; at least one is required. */\nexport const isSeekReplayData: Guard<SeekReplayData> = (value): value is SeekReplayData =>\n\tisRecord(value) &&\n\toptional((v): v is number => isInteger(v) && v >= 0)(value.Frame) &&\n\toptional((v): v is number => isNumber(v) && v >= 0)(value.TimeSeconds) &&\n\t(value.Frame !== undefined || value.TimeSeconds !== undefined);\n\n/** `Speed` must be a finite number ≥ 0. */\nexport const isSetGameSpeedData: Guard<SetGameSpeedData> = (value): value is SetGameSpeedData =>\n\tisRecord(value) && isNumber(value.Speed) && value.Speed >= 0;\n\nexport const isSetHUDVisibilityData: Guard<SetHUDVisibilityData> = (value): value is SetHUDVisibilityData =>\n\tisRecord(value) && isBoolean(value.bVisible);\n\nexport const isSetMatchPausedData: Guard<SetMatchPausedData> = (value): value is SetMatchPausedData =>\n\tisRecord(value) && isBoolean(value.bPaused);\n\n/** Payload guard for every command, keyed by command name. */\nexport const commandDataGuards: { readonly [C in CommandName]: Guard<CommandDataMap[C]> } = {\n\tChangePOV: isChangePOVData,\n\tLoadReplay: isLoadReplayData,\n\tSeekReplay: isSeekReplayData,\n\tSetGameSpeed: isSetGameSpeedData,\n\tSetHUDVisibility: isSetHUDVisibilityData,\n\tSetMatchPaused: isSetMatchPausedData,\n};\n\n/** Checks that a string is a known command name. */\nexport const isCommandName: Guard<CommandName> = oneOf(COMMAND_NAMES);\n\n/** Checks that `data` is a valid payload for the command `name`. */\nexport function isCommandData<C extends CommandName>(name: C, data: unknown): data is CommandDataMap[C] {\n\tconst guard: Guard<CommandDataMap[C]> = commandDataGuards[name];\n\treturn guard(data);\n}\n\n/**\n * Checks that a value is a fully valid command envelope, optionally restricted to a single command name.\n */\nexport function isStatsApiCommand(value: unknown): value is StatsApiCommand;\nexport function isStatsApiCommand<C extends CommandName>(value: unknown, name: C): value is StatsApiCommandOf<C>;\nexport function isStatsApiCommand(value: unknown, name?: CommandName): boolean {\n\tif (!isRecord(value) || !isCommandName(value.Command)) return false;\n\tif (name !== undefined && value.Command !== name) return false;\n\treturn isCommandData(value.Command, value.Data);\n}\n","import { isEventData, isEventName } from './guards/events.js';\nimport { isRecord } from './guards/primitives.js';\nimport type { StatsApiEvent } from './types/events.js';\n\n/** Why a message could not be turned into a {@link StatsApiEvent}. */\nexport type ParseFailureReason =\n\t/** The message is not valid JSON. */\n\t| 'invalid-json'\n\t/** The message is JSON but not an `{ Event, Data }` envelope. */\n\t| 'invalid-envelope'\n\t/** `Data` is a string that is not valid JSON, or does not decode to an object. */\n\t| 'invalid-data-json'\n\t/** `Event` is not one of the documented event names. */\n\t| 'unknown-event'\n\t/** `Data` does not match the documented shape for that event. */\n\t| 'invalid-data';\n\n/** A message that failed to parse or validate. */\nexport interface ParseFailure {\n\tok: false;\n\treason: ParseFailureReason;\n\t/** The original input. */\n\traw: unknown;\n\t/** Event name, when it could be read. */\n\tevent?: string;\n\t/** Decoded payload, when it could be read. */\n\tdata?: unknown;\n}\n\n/** A message that was decoded (and validated, unless disabled). */\nexport interface ParseSuccess {\n\tok: true;\n\tevent: StatsApiEvent;\n}\n\nexport type ParseResult = ParseSuccess | ParseFailure;\n\nexport interface ParseOptions {\n\t/**\n\t * Run the payload type guard for the event.\n\t * When `false`, only the envelope and event name are checked and `Data` is trusted as-is.\n\t * @default true\n\t */\n\tvalidate?: boolean;\n}\n\n/**\n * Decodes a message coming from the Stats API socket.\n *\n * Accepts the raw JSON text, or an already `JSON.parse`d envelope. The game encodes\n * `Data` as a JSON string inside the envelope; it is decoded here. If `Data` is already\n * an object it is used directly.\n *\n * Never throws: check `result.ok`.\n */\nexport function parseEventMessage(raw: unknown, options: ParseOptions = {}): ParseResult {\n\tconst validate = options.validate ?? true;\n\n\tlet envelope: unknown = raw;\n\tif (typeof raw === 'string') {\n\t\ttry {\n\t\t\tenvelope = JSON.parse(raw);\n\t\t} catch {\n\t\t\treturn { ok: false, reason: 'invalid-json', raw };\n\t\t}\n\t}\n\n\tif (!isRecord(envelope) || typeof envelope.Event !== 'string') {\n\t\treturn { ok: false, reason: 'invalid-envelope', raw };\n\t}\n\tconst event = envelope.Event;\n\n\tlet data: unknown = envelope.Data;\n\tif (typeof data === 'string') {\n\t\ttry {\n\t\t\tdata = JSON.parse(data);\n\t\t} catch {\n\t\t\treturn { ok: false, reason: 'invalid-data-json', raw, event, data };\n\t\t}\n\t}\n\tif (!isRecord(data)) {\n\t\treturn { ok: false, reason: 'invalid-data-json', raw, event, data };\n\t}\n\n\tif (!isEventName(event)) {\n\t\treturn { ok: false, reason: 'unknown-event', raw, event, data };\n\t}\n\tif (validate && !isEventData(event, data)) {\n\t\treturn { ok: false, reason: 'invalid-data', raw, event, data };\n\t}\n\n\treturn { ok: true, event: { Event: event, Data: data } as StatsApiEvent };\n}\n","import type { Player, PlayerRef } from './types/common.js';\r\n\r\n/** Compares two players like an `Array.prototype.sort` callback. */\r\nexport type PlayerComparator<T extends PlayerRef = Player> = (a: T, b: T) => number;\r\n\r\n/** Sort direction. */\r\nexport type SortOrder = 'asc' | 'desc';\r\n\r\n/** Numeric `Player` fields that can be used with {@link byStat}. */\r\nexport type PlayerStat = {\r\n\t[K in keyof Player]-?: NonNullable<Player[K]> extends number ? K : never;\r\n}[keyof Player];\r\n\r\nconst direction = (order: SortOrder) => (order === 'asc' ? 1 : -1);\r\n\r\n/** Orders by team: Blue (0) first, or Orange (1) first with `'desc'`. */\r\nexport function byTeam<T extends PlayerRef>(order: SortOrder = 'asc'): PlayerComparator<T> {\r\n\treturn (a, b) => (a.TeamNum - b.TeamNum) * direction(order);\r\n}\r\n\r\n/** Orders by spectator shortcut. */\r\nexport function byShortcut<T extends PlayerRef>(order: SortOrder = 'asc'): PlayerComparator<T> {\r\n\treturn (a, b) => (a.Shortcut - b.Shortcut) * direction(order);\r\n}\r\n\r\n/** Orders by display name (case-insensitive, locale-aware). */\r\nexport function byName<T extends PlayerRef>(order: SortOrder = 'asc'): PlayerComparator<T> {\r\n\treturn (a, b) => a.Name.localeCompare(b.Name, undefined, { sensitivity: 'base' }) * direction(order);\r\n}\r\n\r\n/** Orders by a numeric stat (`Score`, `Goals`, `Boost`…). Highest first by default. Missing values go last. */\r\nexport function byStat(stat: PlayerStat, order: SortOrder = 'desc'): PlayerComparator {\r\n\treturn (a, b) => {\r\n\t\tconst x = a[stat];\r\n\t\tconst y = b[stat];\r\n\t\tif (x === undefined || y === undefined) return x === y ? 0 : x === undefined ? 1 : -1;\r\n\t\treturn (x - y) * direction(order);\r\n\t};\r\n}\r\n\r\n/**\r\n * Returns a new array sorted by the given comparators: the first one decides, the next ones break ties.\r\n * The sort is stable, so players that compare equal keep their original order. The input is not modified.\r\n *\r\n * ```ts\r\n * sortPlayers(data.Players, byTeam(), byStat('Score')); // Blue then Orange, best score first\r\n * sortPlayers(data.Players, (a, b) => a.Demos - b.Demos); // any custom comparator\r\n * ```\r\n */\r\nexport function sortPlayers<T extends PlayerRef>(players: readonly T[], ...comparators: PlayerComparator<T>[]): T[] {\r\n\tconst copy = [...players];\r\n\tif (comparators.length === 0) return copy;\r\n\treturn copy.sort((a, b) => {\r\n\t\tfor (const compare of comparators) {\r\n\t\t\tconst result = compare(a, b);\r\n\t\t\tif (result !== 0) return result;\r\n\t\t}\r\n\t\treturn 0;\r\n\t});\r\n}\r\n","/** Base class for every error thrown or emitted by this library. */\nexport class StatsApiError extends Error {\n\toverride name = 'StatsApiError';\n}\n\n/** Thrown by `send()` when the socket is not open. */\nexport class NotConnectedError extends StatsApiError {\n\toverride name = 'NotConnectedError';\n\tconstructor(message = 'Cannot send a command: the socket is not connected.') {\n\t\tsuper(message);\n\t}\n}\n\n/** Thrown by `send()` when a command name or payload fails validation. */\nexport class CommandValidationError extends StatsApiError {\n\toverride name = 'CommandValidationError';\n\tconstructor(\n\t\tmessage: string,\n\t\t/** The command name that was passed. */\n\t\treadonly command: unknown,\n\t\t/** The payload that was passed. */\n\t\treadonly data: unknown,\n\t) {\n\t\tsuper(message);\n\t}\n}\n\n/** Emitted on `error` when the WebSocket reports a failure. */\nexport class ConnectionError extends StatsApiError {\n\toverride name = 'ConnectionError';\n}\n\n/** Emitted on `error` when an event listener throws. */\nexport class ListenerError extends StatsApiError {\n\toverride name = 'ListenerError';\n\tconstructor(\n\t\t/** The listener event name that was being dispatched. */\n\t\treadonly eventName: string,\n\t\tcause: unknown,\n\t) {\n\t\tsuper(`A listener for \"${eventName}\" threw an error.`, { cause });\n\t}\n}\n","import { CommandValidationError, ConnectionError, ListenerError, NotConnectedError } from './errors.js';\r\nimport { isCommandData, isCommandName, isStatsApiCommand } from './guards/commands.js';\r\nimport { isRecord } from './guards/primitives.js';\r\nimport { parseEventMessage, type ParseFailure } from './parse.js';\r\nimport type { CommandDataMap, CommandName, StatsApiCommand } from './types/commands.js';\r\nimport type { EventDataMap, EventName, StatsApiEvent, StatsApiEventOf } from './types/events.js';\r\nimport { sortPlayers, type PlayerComparator } from './sort.js';\r\nimport type { EnsureStatsApiOptions, EnsureStatsApiResult } from './node/config.js';\r\n\r\n// ---------------------------------------------------------------------------\r\n// WebSocket abstraction\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The subset of the standard `WebSocket` API this client relies on.\r\n * Satisfied by the browser/Node.js (>= 22) global `WebSocket` and by the `ws` package.\r\n */\r\nexport interface WebSocketLike {\r\n\treadonly readyState: number;\r\n\tsend(data: string): void;\r\n\tclose(code?: number, reason?: string): void;\r\n\taddEventListener(type: 'open', listener: () => void): void;\r\n\taddEventListener(type: 'close', listener: (event: { code: number; reason: string }) => void): void;\r\n\taddEventListener(type: 'error', listener: (event: unknown) => void): void;\r\n\taddEventListener(type: 'message', listener: (event: { data: unknown }) => void): void;\r\n}\r\n\r\n/** A constructor producing a {@link WebSocketLike}. */\r\nexport type WebSocketConstructor = new (url: string) => WebSocketLike;\r\n\r\nconst WS_OPEN = 1;\r\n\r\n// ---------------------------------------------------------------------------\r\n// Options\r\n// ---------------------------------------------------------------------------\r\n\r\nexport interface ReconnectOptions {\r\n\t/** Delay before the first reconnection attempt, in ms. @default 1000 */\r\n\tinitialDelay?: number;\r\n\t/** Upper bound for the delay between attempts, in ms. @default 30000 */\r\n\tmaxDelay?: number;\r\n\t/** Multiplier applied to the delay after every failed attempt. @default 2 */\r\n\tfactor?: number;\r\n\t/** Maximum consecutive attempts before giving up. @default Infinity */\r\n\tmaxAttempts?: number;\r\n}\r\n\r\nexport interface StatsApiClientOptions {\r\n\t/** Host of the Rocket League client. @default '127.0.0.1' */\r\n\thost?: string;\r\n\t/** The `WebPort` configured in `TAStatsAPI.ini`. @default 49124 */\r\n\tport?: number;\r\n\t/** Use `wss://` instead of `ws://`. @default false */\r\n\tsecure?: boolean;\r\n\t/** Full WebSocket URL. Overrides `host`, `port` and `secure` when set. */\r\n\turl?: string;\r\n\t/**\r\n\t * Connect automatically after construction. Set to `false` to call {@link RocketLeagueStatsClient.connect} yourself.\r\n\t * The connection starts on the next microtask, so listeners attached right after `new` still receive `connect`.\r\n\t * @default true\r\n\t */\r\n\tautoConnect?: boolean;\r\n\t/**\r\n\t * Reconnect automatically when the connection drops or cannot be established\r\n\t * (e.g. the game is not running yet). `false` disables it.\r\n\t * @default true\r\n\t */\r\n\treconnect?: boolean | ReconnectOptions;\r\n\t/**\r\n\t * Validate every incoming event with its type guard. Invalid events are not dispatched\r\n\t * and are reported on `invalidMessage` instead.\r\n\t * @default true\r\n\t */\r\n\tvalidateEvents?: boolean;\r\n\t/**\r\n\t * Drop the fake `GoalScored` event the game sends when a replay ends\r\n\t * (its `Scorer` is `{ Name: '', Shortcut: 0, TeamNum: 0 }`). See {@link isReplayEndGoal}.\r\n\t * @default true\r\n\t */\r\n\tignoreReplayEndGoal?: boolean;\r\n\t/**\r\n\t * Sort `UpdateState.Players` before listeners receive it. Pass one comparator, or several to break ties\r\n\t * (e.g. `[byTeam(), byStat('Score')]`). When omitted, players keep the order sent by the game.\r\n\t */\r\n\tsortPlayers?: PlayerComparator | PlayerComparator[];\r\n\t/**\r\n\t * WebSocket implementation. Defaults to `globalThis.WebSocket`\r\n\t * (browsers, Node.js >= 22). Pass `WebSocket` from the `ws` package for older runtimes.\r\n\t */\r\n\tWebSocket?: WebSocketConstructor;\r\n\t/**\r\n\t * **Node.js only** (ignored in browsers). Check the Stats API `.ini` file (`TAStatsAPI.ini`, else\r\n\t * `DefaultStatsAPI.ini`) on construction and, if `PacketSendRate` is `0` or less, missing, or above `120`, set it to `30` and\r\n\t * warn that the game must be restarted. Pass options to customize it, or `false` to disable it.\r\n\t * @default true\r\n\t */\r\n\tensureStatsApi?: boolean | EnsureStatsApiOptions;\r\n}\r\n\r\n/**\r\n * `true` for the fake `GoalScored` event the game sends when a replay ends:\r\n * its `Scorer` is `{ Name: '', Shortcut: 0, TeamNum: 0 }`.\r\n */\r\nexport function isReplayEndGoal(event: StatsApiEvent): boolean {\r\n\tif (event.Event !== 'GoalScored') return false;\r\n\tconst { Name, Shortcut, TeamNum } = event.Data.Scorer;\r\n\treturn Name === '' && Shortcut === 0 && TeamNum === 0;\r\n}\r\n\r\n/** `true` when running on Node.js (where the `.ini` file can be read). */\r\nfunction isNodeRuntime(): boolean {\r\n\tconst proc = (globalThis as { process?: { versions?: { node?: unknown } } }).process;\r\n\treturn typeof proc?.versions?.node === 'string';\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Listener types\r\n// ---------------------------------------------------------------------------\r\n\r\n/** Connection state of the client. */\r\nexport type ConnectionState = 'idle' | 'connecting' | 'open' | 'closed';\r\n\r\nexport interface DisconnectInfo {\r\n\tcode: number;\r\n\treason: string;\r\n\t/** `true` when a reconnection attempt has been scheduled. */\r\n\twillReconnect: boolean;\r\n}\r\n\r\nexport interface ReconnectInfo {\r\n\t/** 1-based attempt number. */\r\n\tattempt: number;\r\n\t/** Delay before this attempt, in ms. */\r\n\tdelay: number;\r\n}\r\n\r\n/** Listener for a specific game event. Receives the typed payload and the whole decoded message. */\r\nexport type GameEventListener<E extends EventName> = (data: EventDataMap[E], event: StatsApiEventOf<E>) => void;\r\n\r\n/** Listener receiving every game event as a discriminated union. */\r\nexport type AnyEventListener = (event: StatsApiEvent) => void;\r\n\r\n/** Client lifecycle events (lowercase, so they never collide with game events). */\r\nexport interface LifecycleListenerMap {\r\n\t/** The socket is open. */\r\n\tconnect: () => void;\r\n\t/** The socket closed. */\r\n\tdisconnect: (info: DisconnectInfo) => void;\r\n\t/** A reconnection attempt has been scheduled. */\r\n\treconnecting: (info: ReconnectInfo) => void;\r\n\t/** A connection error or a listener error happened. */\r\n\terror: (error: Error) => void;\r\n\t/** A message could not be decoded, has an unknown event name, or failed validation. */\r\n\tinvalidMessage: (failure: ParseFailure) => void;\r\n}\r\n\r\n/** Every listener accepted by {@link RocketLeagueStatsClient.on}. */\r\nexport type ClientListenerMap = { [E in EventName]: GameEventListener<E> } & LifecycleListenerMap;\r\n\r\n/** Any name accepted by {@link RocketLeagueStatsClient.on}. */\r\nexport type ClientEventName = keyof ClientListenerMap;\r\n\r\n/** Call it to remove the listener. */\r\nexport type Unsubscribe = () => void;\r\n\r\ntype AnyFunction = (...args: never[]) => void;\r\n\r\n// ---------------------------------------------------------------------------\r\n// Client\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * WebSocket client for the Rocket League Stats API.\r\n *\r\n * ```ts\r\n * const client = new RocketLeagueStatsClient();\r\n *\r\n * client.on('GoalScored', (data) => console.log(`${data.Scorer.Name} scored!`));\r\n * client.onAny((event) => console.log(event.Event));\r\n * client.send('ChangePOV', { Focus: 'Ball' });\r\n * ```\r\n *\r\n * @see https://www.rocketleague.com/developer/stats-api\r\n */\r\nexport class RocketLeagueStatsClient {\r\n\t/** The WebSocket URL this client connects to. */\r\n\treadonly url: string;\r\n\r\n\t/**\r\n\t * Result of the `.ini` check (see the `ensureStatsApi` option). `undefined` outside Node.js or when\r\n\t * `ensureStatsApi: false`. Never rejects.\r\n\t */\r\n\treadonly statsApiConfig: Promise<EnsureStatsApiResult> | undefined;\r\n\r\n\treadonly #autoConnect: boolean;\r\n\treadonly #reconnect: Required<ReconnectOptions> | null;\r\n\treadonly #validateEvents: boolean;\r\n\treadonly #ignoreReplayEndGoal: boolean;\r\n\treadonly #playerComparators: PlayerComparator[];\r\n\treadonly #WebSocket: WebSocketConstructor | undefined;\r\n\r\n\t#socket: WebSocketLike | null = null;\r\n\t#state: ConnectionState = 'idle';\r\n\t#manualClose = false;\r\n\t#reconnectAttempts = 0;\r\n\t#reconnectTimer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n\treadonly #listeners = new Map<string, Set<AnyFunction>>();\r\n\treadonly #anyListeners = new Set<AnyEventListener>();\r\n\r\n\tconstructor(options: StatsApiClientOptions = {}) {\r\n\t\tconst protocol = options.secure ? 'wss' : 'ws';\r\n\t\tthis.url = options.url ?? `${protocol}://${options.host ?? '127.0.0.1'}:${options.port ?? 49124}`;\r\n\t\tthis.#autoConnect = options.autoConnect ?? true;\r\n\t\tthis.#validateEvents = options.validateEvents ?? true;\r\n\t\tthis.#ignoreReplayEndGoal = options.ignoreReplayEndGoal ?? true;\r\n\t\tthis.#playerComparators = options.sortPlayers === undefined ? [] : [options.sortPlayers].flat();\r\n\t\tthis.#WebSocket = options.WebSocket ?? (globalThis as { WebSocket?: WebSocketConstructor }).WebSocket;\r\n\r\n\t\tconst reconnect = options.reconnect ?? true;\r\n\t\tthis.#reconnect =\r\n\t\t\treconnect === false\r\n\t\t\t\t? null\r\n\t\t\t\t: {\r\n\t\t\t\t\t\tinitialDelay: 1000,\r\n\t\t\t\t\t\tmaxDelay: 30_000,\r\n\t\t\t\t\t\tfactor: 2,\r\n\t\t\t\t\t\tmaxAttempts: Number.POSITIVE_INFINITY,\r\n\t\t\t\t\t\t...(reconnect === true ? {} : reconnect),\r\n\t\t\t\t\t};\r\n\r\n\t\tconst ensureStatsApi = options.ensureStatsApi ?? true;\r\n\t\tif (ensureStatsApi !== false && isNodeRuntime()) {\r\n\t\t\tconst ensureOptions = ensureStatsApi === true ? {} : ensureStatsApi;\r\n\t\t\tconst rate = ensureOptions.packetSendRate;\r\n\t\t\tif (rate !== undefined && !(Number.isFinite(rate) && rate > 0 && rate <= 120)) {\r\n\t\t\t\tthrow new RangeError(`packetSendRate must be between 1 and 120, got ${rate}.`);\r\n\t\t\t}\r\n\t\t\t// Loaded lazily so the Node-only code (node:fs) is never evaluated in browsers.\r\n\t\t\tthis.statsApiConfig = import('./node/config.js').then(({ ensureStatsApiEnabled }) => ensureStatsApiEnabled(ensureOptions));\r\n\t\t} else {\r\n\t\t\tthis.statsApiConfig = undefined;\r\n\t\t}\r\n\r\n\t\tif (this.#autoConnect) {\r\n\t\t\tqueueMicrotask(() => {\r\n\t\t\t\tif (this.#state === 'idle' && !this.#manualClose) this.connect();\r\n\t\t\t});\r\n\t\t}\r\n\t}\r\n\r\n\t/** Current connection state. */\r\n\tget state(): ConnectionState {\r\n\t\treturn this.#state;\r\n\t}\r\n\r\n\t/** `true` when the socket is open and commands can be sent. */\r\n\tget isConnected(): boolean {\r\n\t\treturn this.#state === 'open' && this.#socket?.readyState === WS_OPEN;\r\n\t}\r\n\r\n\t// -----------------------------------------------------------------------\r\n\t// Connection\r\n\t// -----------------------------------------------------------------------\r\n\r\n\t/**\r\n\t * Opens the connection. Does nothing if already connecting or connected.\r\n\t * Called automatically unless `autoConnect: false` was passed.\r\n\t */\r\n\tconnect(): void {\r\n\t\tif (this.#state === 'connecting' || this.#state === 'open') return;\r\n\t\tthis.#manualClose = false;\r\n\t\tthis.#clearReconnectTimer();\r\n\r\n\t\tconst WebSocketImpl = this.#WebSocket;\r\n\t\tif (!WebSocketImpl) {\r\n\t\t\tthrow new ConnectionError(\r\n\t\t\t\t'No WebSocket implementation found. Use Node.js >= 22, or pass one via the `WebSocket` option (e.g. from the \"ws\" package).',\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\tthis.#state = 'connecting';\r\n\t\tlet socket: WebSocketLike;\r\n\t\ttry {\r\n\t\t\tsocket = new WebSocketImpl(this.url);\r\n\t\t} catch (cause) {\r\n\t\t\tthis.#state = 'closed';\r\n\t\t\tthis.#emitError(new ConnectionError(`Failed to create WebSocket for ${this.url}.`, { cause }));\r\n\t\t\tthis.#scheduleReconnect();\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tthis.#socket = socket;\r\n\r\n\t\tsocket.addEventListener('open', () => {\r\n\t\t\tif (this.#socket !== socket) return;\r\n\t\t\tthis.#state = 'open';\r\n\t\t\tthis.#reconnectAttempts = 0;\r\n\t\t\tthis.#emit('connect');\r\n\t\t});\r\n\t\tsocket.addEventListener('message', (event) => {\r\n\t\t\tif (this.#socket !== socket) return;\r\n\t\t\tvoid this.#handleRawMessage(event.data);\r\n\t\t});\r\n\t\tsocket.addEventListener('error', (cause) => {\r\n\t\t\tif (this.#socket !== socket) return;\r\n\t\t\tthis.#emitError(new ConnectionError(`WebSocket error on ${this.url}.`, { cause }));\r\n\t\t});\r\n\t\tsocket.addEventListener('close', (event) => {\r\n\t\t\tif (this.#socket !== socket) return;\r\n\t\t\tthis.#socket = null;\r\n\t\t\tthis.#state = 'closed';\r\n\t\t\tconst willReconnect = this.#scheduleReconnect(true);\r\n\t\t\tthis.#emit('disconnect', { code: event.code, reason: event.reason, willReconnect });\r\n\t\t});\r\n\t}\r\n\r\n\t/** Closes the connection and stops any pending reconnection. */\r\n\tdisconnect(code = 1000, reason = 'Client disconnect'): void {\r\n\t\tthis.#manualClose = true;\r\n\t\tthis.#clearReconnectTimer();\r\n\t\tconst socket = this.#socket;\r\n\t\tif (!socket) {\r\n\t\t\tif (this.#state !== 'idle') this.#state = 'closed';\r\n\t\t\treturn;\r\n\t\t}\r\n\t\t// Keep the socket attached so its `close` event still emits `disconnect`.\r\n\t\tsocket.close(code, reason);\r\n\t}\r\n\r\n\t#scheduleReconnect(deferEmit = false): boolean {\r\n\t\tconst options = this.#reconnect;\r\n\t\tif (!options || this.#manualClose || this.#reconnectAttempts >= options.maxAttempts) return false;\r\n\r\n\t\tconst attempt = ++this.#reconnectAttempts;\r\n\t\tconst delay = Math.min(options.initialDelay * options.factor ** (attempt - 1), options.maxDelay);\r\n\t\tthis.#reconnectTimer = setTimeout(() => {\r\n\t\t\tthis.#reconnectTimer = null;\r\n\t\t\tthis.connect();\r\n\t\t}, delay);\r\n\r\n\t\tconst notify = () => this.#emit('reconnecting', { attempt, delay });\r\n\t\tif (deferEmit) queueMicrotask(notify);\r\n\t\telse notify();\r\n\t\treturn true;\r\n\t}\r\n\r\n\t#clearReconnectTimer(): void {\r\n\t\tif (this.#reconnectTimer !== null) {\r\n\t\t\tclearTimeout(this.#reconnectTimer);\r\n\t\t\tthis.#reconnectTimer = null;\r\n\t\t}\r\n\t}\r\n\r\n\t// -----------------------------------------------------------------------\r\n\t// Incoming events\r\n\t// -----------------------------------------------------------------------\r\n\r\n\tasync #handleRawMessage(raw: unknown): Promise<void> {\r\n\t\tlet text: unknown = raw;\r\n\t\tif (raw instanceof ArrayBuffer || ArrayBuffer.isView(raw)) {\r\n\t\t\ttext = new TextDecoder().decode(raw);\r\n\t\t} else if (typeof Blob !== 'undefined' && raw instanceof Blob) {\r\n\t\t\ttext = await raw.text();\r\n\t\t}\r\n\t\tthis.handleMessage(text);\r\n\t}\r\n\r\n\t/**\r\n\t * Decodes, validates and dispatches one raw message. Called for every socket message;\r\n\t * exposed so messages from another transport (e.g. the TCP port, a recording) can be fed in.\r\n\t */\r\n\thandleMessage(raw: unknown): void {\r\n\t\tconst result = parseEventMessage(raw, { validate: this.#validateEvents });\r\n\t\tif (!result.ok) {\r\n\t\t\tthis.#emit('invalidMessage', result);\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tconst { event } = result;\r\n\t\tif (this.#ignoreReplayEndGoal && isReplayEndGoal(event)) return;\r\n\t\tif (this.#playerComparators.length > 0 && event.Event === 'UpdateState' && Array.isArray(event.Data.Players)) {\r\n\t\t\tevent.Data.Players = sortPlayers(event.Data.Players, ...this.#playerComparators);\r\n\t\t}\r\n\t\t// `event.Data` always matches `event.Event` (guaranteed by the parser), so dispatch untyped.\r\n\t\tthis.#dispatch(event.Event, [event.Data, event]);\r\n\t\tfor (const listener of [...this.#anyListeners]) {\r\n\t\t\tthis.#invoke('*', listener, [event]);\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Listens to a game event (e.g. `'GoalScored'`) or a lifecycle event (e.g. `'connect'`).\r\n\t * The listener's arguments are typed from the event name.\r\n\t *\r\n\t * @returns A function that removes the listener.\r\n\t */\r\n\ton<K extends ClientEventName>(name: K, listener: ClientListenerMap[K]): Unsubscribe {\r\n\t\tlet set = this.#listeners.get(name);\r\n\t\tif (!set) {\r\n\t\t\tset = new Set();\r\n\t\t\tthis.#listeners.set(name, set);\r\n\t\t}\r\n\t\tset.add(listener as AnyFunction);\r\n\t\treturn () => this.off(name, listener);\r\n\t}\r\n\r\n\t/** Like {@link on}, but the listener is removed after its first call. */\r\n\tonce<K extends ClientEventName>(name: K, listener: ClientListenerMap[K]): Unsubscribe {\r\n\t\tconst wrapper = ((...args: never[]) => {\r\n\t\t\tunsubscribe();\r\n\t\t\t(listener as AnyFunction)(...args);\r\n\t\t}) as ClientListenerMap[K];\r\n\t\tconst unsubscribe = this.on(name, wrapper);\r\n\t\treturn unsubscribe;\r\n\t}\r\n\r\n\t/** Removes a listener added with {@link on}. */\r\n\toff<K extends ClientEventName>(name: K, listener: ClientListenerMap[K]): void {\r\n\t\tthis.#listeners.get(name)?.delete(listener as AnyFunction);\r\n\t}\r\n\r\n\t/**\r\n\t * Listens to **every** game event. The argument is a discriminated union: narrow it with `event.Event`.\r\n\t *\r\n\t * ```ts\r\n\t * client.onAny((event) => {\r\n\t * if (event.Event === 'StatfeedEvent') console.log(event.Data.EventName);\r\n\t * });\r\n\t * ```\r\n\t *\r\n\t * @returns A function that removes the listener.\r\n\t */\r\n\tonAny(listener: AnyEventListener): Unsubscribe {\r\n\t\tthis.#anyListeners.add(listener);\r\n\t\treturn () => this.offAny(listener);\r\n\t}\r\n\r\n\t/** Removes a listener added with {@link onAny}. */\r\n\toffAny(listener: AnyEventListener): void {\r\n\t\tthis.#anyListeners.delete(listener);\r\n\t}\r\n\r\n\t/** Removes every listener, or every listener of one name. */\r\n\tremoveAllListeners(name?: ClientEventName): void {\r\n\t\tif (name === undefined) {\r\n\t\t\tthis.#listeners.clear();\r\n\t\t\tthis.#anyListeners.clear();\r\n\t\t} else {\r\n\t\t\tthis.#listeners.delete(name);\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Resolves with the payload of the next occurrence of a game event.\r\n\t * Rejects after `timeout` ms if provided.\r\n\t */\r\n\twaitFor<E extends EventName>(name: E, options: { timeout?: number } = {}): Promise<EventDataMap[E]> {\r\n\t\treturn new Promise((resolve, reject) => {\r\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\r\n\t\t\tconst unsubscribe = this.once(name, ((data: EventDataMap[E]) => {\r\n\t\t\t\tif (timer) clearTimeout(timer);\r\n\t\t\t\tresolve(data);\r\n\t\t\t}) as unknown as ClientListenerMap[E]);\r\n\t\t\tif (options.timeout !== undefined) {\r\n\t\t\t\ttimer = setTimeout(() => {\r\n\t\t\t\t\tunsubscribe();\r\n\t\t\t\t\treject(new Error(`Timed out after ${options.timeout}ms waiting for \"${name}\".`));\r\n\t\t\t\t}, options.timeout);\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\r\n\t#emit<K extends keyof LifecycleListenerMap>(name: K, ...args: Parameters<LifecycleListenerMap[K]>): void {\r\n\t\tthis.#dispatch(name, args);\r\n\t}\r\n\r\n\t#dispatch(name: string, args: unknown[]): void {\r\n\t\tconst set = this.#listeners.get(name);\r\n\t\tif (!set) return;\r\n\t\tfor (const listener of [...set]) {\r\n\t\t\tthis.#invoke(name, listener, args);\r\n\t\t}\r\n\t}\r\n\r\n\t#invoke(name: string, listener: AnyFunction, args: unknown[]): void {\r\n\t\ttry {\r\n\t\t\t(listener as (...a: unknown[]) => void)(...args);\r\n\t\t} catch (cause) {\r\n\t\t\tif (name === 'error') return; // never loop on a failing error listener\r\n\t\t\tthis.#emitError(new ListenerError(name, cause));\r\n\t\t}\r\n\t}\r\n\r\n\t/** Errors are only reported to `error` listeners; without one they are silently dropped. */\r\n\t#emitError(error: Error): void {\r\n\t\tthis.#emit('error', error);\r\n\t}\r\n\r\n\t// -----------------------------------------------------------------------\r\n\t// Commands\r\n\t// -----------------------------------------------------------------------\r\n\r\n\t/**\r\n\t * Sends a command to the game. The payload is type-checked at compile time and\r\n\t * validated with its guard at runtime before being sent.\r\n\t *\r\n\t * ```ts\r\n\t * client.send('ChangePOV', { Focus: '1', Perspective: 'PlayerView' });\r\n\t * client.send({ Command: 'SetHUDVisibility', Data: { bVisible: false } });\r\n\t * ```\r\n\t *\r\n\t * @throws {CommandValidationError} If the command name or payload is invalid.\r\n\t * @throws {NotConnectedError} If the socket is not open.\r\n\t */\r\n\tsend<C extends CommandName>(command: C, data: CommandDataMap[C]): void;\r\n\tsend(command: StatsApiCommand): void;\r\n\tsend(commandOrEnvelope: CommandName | StatsApiCommand, maybeData?: unknown): void {\r\n\t\tconst [command, data]: [unknown, unknown] =\r\n\t\t\ttypeof commandOrEnvelope === 'string'\r\n\t\t\t\t? [commandOrEnvelope, maybeData]\r\n\t\t\t\t: isRecord(commandOrEnvelope)\r\n\t\t\t\t\t? [commandOrEnvelope.Command, commandOrEnvelope.Data]\r\n\t\t\t\t\t: [commandOrEnvelope, undefined];\r\n\r\n\t\tif (!isCommandName(command)) {\r\n\t\t\tthrow new CommandValidationError(`Unknown command \"${String(command)}\".`, command, data);\r\n\t\t}\r\n\t\tif (!isCommandData(command, data)) {\r\n\t\t\tthrow new CommandValidationError(`Invalid payload for command \"${command}\".`, command, data);\r\n\t\t}\r\n\r\n\t\tconst envelope = { Command: command, Data: data };\r\n\t\t// Defensive: guarantees the envelope as a whole is valid before it reaches the game.\r\n\t\tif (!isStatsApiCommand(envelope)) {\r\n\t\t\tthrow new CommandValidationError(`Invalid command envelope for \"${command}\".`, command, data);\r\n\t\t}\r\n\r\n\t\tconst socket = this.#socket;\r\n\t\tif (!socket || !this.isConnected) throw new NotConnectedError();\r\n\t\tsocket.send(JSON.stringify(envelope));\r\n\t}\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/node/config.ts
|
|
2
|
+
import { access, readdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var STATS_API_SECTION = "TAGame.MatchStatsExporter_TA";
|
|
5
|
+
var DEFAULT_PACKET_SEND_RATE = 30;
|
|
6
|
+
var MAX_PACKET_SEND_RATE = 120;
|
|
7
|
+
var STATS_API_INI_FILES = ["TAStatsAPI.ini", "DefaultStatsAPI.ini"];
|
|
8
|
+
var EPIC_APP_NAME = "Sugar";
|
|
9
|
+
var SECTION_RE = /^\s*\[([^\]]+)\]\s*$/;
|
|
10
|
+
var KEY_RE = /^(\s*PacketSendRate\s*=\s*)([^;\r\n]*?)(\s*(?:;.*)?)$/i;
|
|
11
|
+
function editPacketSendRate(content, rate = DEFAULT_PACKET_SEND_RATE) {
|
|
12
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
13
|
+
const lines = content.split(/\r?\n/);
|
|
14
|
+
let sectionStart = -1;
|
|
15
|
+
let sectionEnd = lines.length;
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
const match = SECTION_RE.exec(lines[i]);
|
|
18
|
+
if (!match) continue;
|
|
19
|
+
if (sectionStart !== -1) {
|
|
20
|
+
sectionEnd = i;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
if (match[1].trim().toLowerCase() === STATS_API_SECTION.toLowerCase()) sectionStart = i;
|
|
24
|
+
}
|
|
25
|
+
let keyLine = -1;
|
|
26
|
+
let previous;
|
|
27
|
+
if (sectionStart !== -1) {
|
|
28
|
+
for (let i = sectionStart + 1; i < sectionEnd; i++) {
|
|
29
|
+
const match = KEY_RE.exec(lines[i]);
|
|
30
|
+
if (match) {
|
|
31
|
+
keyLine = i;
|
|
32
|
+
const value = Number.parseFloat(match[2]);
|
|
33
|
+
previous = Number.isNaN(value) ? void 0 : value;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const needsUpdate = previous === void 0 || previous <= 0 || previous > MAX_PACKET_SEND_RATE;
|
|
39
|
+
if (!needsUpdate) return { previous, needsUpdate, content };
|
|
40
|
+
if (keyLine !== -1) {
|
|
41
|
+
lines[keyLine] = lines[keyLine].replace(KEY_RE, (_, prefix, _value, suffix) => `${prefix}${rate}${suffix}`);
|
|
42
|
+
} else if (sectionStart !== -1) {
|
|
43
|
+
lines.splice(sectionStart + 1, 0, `PacketSendRate=${rate}`);
|
|
44
|
+
} else {
|
|
45
|
+
const hasTrailingNewline = lines.length > 0 && lines.at(-1) === "";
|
|
46
|
+
if (hasTrailingNewline) lines.pop();
|
|
47
|
+
if (lines.length > 0 && lines.at(-1).trim() !== "") lines.push("");
|
|
48
|
+
lines.push(`[${STATS_API_SECTION}]`, `PacketSendRate=${rate}`);
|
|
49
|
+
if (hasTrailingNewline) lines.push("");
|
|
50
|
+
}
|
|
51
|
+
return { previous, needsUpdate, content: lines.join(eol) };
|
|
52
|
+
}
|
|
53
|
+
async function exists(file) {
|
|
54
|
+
try {
|
|
55
|
+
await access(file);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function readJson(file) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
64
|
+
} catch {
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function epicInstallLocation(value) {
|
|
69
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
70
|
+
const { AppName, InstallLocation } = value;
|
|
71
|
+
return AppName === EPIC_APP_NAME && typeof InstallLocation === "string" ? InstallLocation : void 0;
|
|
72
|
+
}
|
|
73
|
+
async function findEpicInstalls(programData) {
|
|
74
|
+
const found = [];
|
|
75
|
+
const launcherInstalled = await readJson(path.join(programData, "Epic", "UnrealEngineLauncher", "LauncherInstalled.dat"));
|
|
76
|
+
const list = launcherInstalled?.InstallationList;
|
|
77
|
+
if (Array.isArray(list)) {
|
|
78
|
+
for (const item of list) {
|
|
79
|
+
const location = epicInstallLocation(item);
|
|
80
|
+
if (location) found.push(location);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const manifestsDir = path.join(programData, "Epic", "EpicGamesLauncher", "Data", "Manifests");
|
|
84
|
+
try {
|
|
85
|
+
for (const file of await readdir(manifestsDir)) {
|
|
86
|
+
if (!file.toLowerCase().endsWith(".item")) continue;
|
|
87
|
+
const location = epicInstallLocation(await readJson(path.join(manifestsDir, file)));
|
|
88
|
+
if (location) found.push(location);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
async function findSteamInstalls(steamDir) {
|
|
95
|
+
const libraries = [steamDir];
|
|
96
|
+
try {
|
|
97
|
+
const vdf = await readFile(path.join(steamDir, "steamapps", "libraryfolders.vdf"), "utf8");
|
|
98
|
+
for (const match of vdf.matchAll(/"path"\s+"([^"]+)"/g)) libraries.push(match[1].replace(/\\\\/g, "\\"));
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
return libraries.map((library) => path.join(library, "steamapps", "common", "rocketleague"));
|
|
102
|
+
}
|
|
103
|
+
async function findRocketLeagueInstallDirs() {
|
|
104
|
+
if (process.platform !== "win32") return [];
|
|
105
|
+
const env = process.env;
|
|
106
|
+
const programData = env.ProgramData ?? "C:\\ProgramData";
|
|
107
|
+
const programFiles = env.ProgramFiles ?? "C:\\Program Files";
|
|
108
|
+
const programFilesX86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
109
|
+
const candidates = [
|
|
110
|
+
...await findEpicInstalls(programData),
|
|
111
|
+
path.join(programFiles, "Epic Games", "rocketleague"),
|
|
112
|
+
...await findSteamInstalls(path.join(programFilesX86, "Steam"))
|
|
113
|
+
];
|
|
114
|
+
return [...new Set(candidates.map((dir) => path.normalize(dir)))];
|
|
115
|
+
}
|
|
116
|
+
function statsApiIniPaths(installDir) {
|
|
117
|
+
return STATS_API_INI_FILES.map((file) => path.join(installDir, "TAGame", "Config", file));
|
|
118
|
+
}
|
|
119
|
+
async function findStatsApiIni(installDir) {
|
|
120
|
+
const dirs = installDir ? [installDir] : await findRocketLeagueInstallDirs();
|
|
121
|
+
const searched = [];
|
|
122
|
+
for (const dir of dirs) {
|
|
123
|
+
for (const file of statsApiIniPaths(dir)) {
|
|
124
|
+
searched.push(file);
|
|
125
|
+
if (await exists(file)) return { iniPath: file, searched };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { iniPath: void 0, searched };
|
|
129
|
+
}
|
|
130
|
+
function describeProblem(previous) {
|
|
131
|
+
return previous !== void 0 && previous > MAX_PACKET_SEND_RATE ? `PacketSendRate=${previous} is above the maximum of ${MAX_PACKET_SEND_RATE}` : `The Rocket League Stats API is disabled (PacketSendRate=${previous ?? 0})`;
|
|
132
|
+
}
|
|
133
|
+
async function ensureStatsApiEnabled(options = {}) {
|
|
134
|
+
const rate = options.packetSendRate ?? DEFAULT_PACKET_SEND_RATE;
|
|
135
|
+
if (!Number.isFinite(rate) || rate <= 0 || rate > MAX_PACKET_SEND_RATE) {
|
|
136
|
+
throw new RangeError(`packetSendRate must be between 1 and ${MAX_PACKET_SEND_RATE}, got ${rate}.`);
|
|
137
|
+
}
|
|
138
|
+
const warn = options.warn === false ? () => {
|
|
139
|
+
} : options.warn ?? ((message) => console.warn(message));
|
|
140
|
+
let iniPath = options.iniPath;
|
|
141
|
+
let searched = iniPath ? [iniPath] : [];
|
|
142
|
+
if (iniPath === void 0) {
|
|
143
|
+
({ iniPath, searched } = await findStatsApiIni(options.installDir));
|
|
144
|
+
}
|
|
145
|
+
let content;
|
|
146
|
+
let encoding;
|
|
147
|
+
try {
|
|
148
|
+
if (!iniPath) throw new Error("not found");
|
|
149
|
+
const buffer = await readFile(iniPath);
|
|
150
|
+
encoding = buffer[0] === 255 && buffer[1] === 254 ? "utf16le" : "utf8";
|
|
151
|
+
content = buffer.toString(encoding);
|
|
152
|
+
} catch {
|
|
153
|
+
return { status: "not-found", searched };
|
|
154
|
+
}
|
|
155
|
+
const edit = editPacketSendRate(content, rate);
|
|
156
|
+
if (!edit.needsUpdate) return { status: "enabled", iniPath, packetSendRate: edit.previous };
|
|
157
|
+
try {
|
|
158
|
+
await writeFile(iniPath, edit.content, encoding);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
warn(
|
|
161
|
+
`[@juande_pl/rocketleague-stats-api-client] ${describeProblem(edit.previous)}, but "${iniPath}" could not be written. Set PacketSendRate=${rate} manually (you may need administrator rights) and restart the game.`
|
|
162
|
+
);
|
|
163
|
+
return { status: "write-failed", iniPath, previous: edit.previous, error };
|
|
164
|
+
}
|
|
165
|
+
warn(
|
|
166
|
+
`[@juande_pl/rocketleague-stats-api-client] ${describeProblem(edit.previous)}. PacketSendRate has been set to ${rate} in "${iniPath}". Restart Rocket League for the Stats API to work.`
|
|
167
|
+
);
|
|
168
|
+
return { status: "updated", iniPath, previous: edit.previous, packetSendRate: rate, restartRequired: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export {
|
|
172
|
+
STATS_API_SECTION,
|
|
173
|
+
DEFAULT_PACKET_SEND_RATE,
|
|
174
|
+
MAX_PACKET_SEND_RATE,
|
|
175
|
+
STATS_API_INI_FILES,
|
|
176
|
+
editPacketSendRate,
|
|
177
|
+
findRocketLeagueInstallDirs,
|
|
178
|
+
statsApiIniPaths,
|
|
179
|
+
findStatsApiIni,
|
|
180
|
+
ensureStatsApiEnabled
|
|
181
|
+
};
|
|
182
|
+
//# sourceMappingURL=chunk-IUPCJADJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/node/config.ts"],"sourcesContent":["import { access, readdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\n\n/** Section of the Stats API `.ini` file that holds the exporter settings. */\nexport const STATS_API_SECTION = 'TAGame.MatchStatsExporter_TA';\n\n/** `PacketSendRate` written when the current value is invalid (`<= 0`, missing, or above the maximum). */\nexport const DEFAULT_PACKET_SEND_RATE = 30;\n\n/** Highest `PacketSendRate` the game accepts. */\nexport const MAX_PACKET_SEND_RATE = 120;\n\n/** `.ini` file names, in lookup order. `TAStatsAPI.ini` wins over `DefaultStatsAPI.ini`. */\nexport const STATS_API_INI_FILES = ['TAStatsAPI.ini', 'DefaultStatsAPI.ini'] as const;\n\n/** Epic Games Store app name of Rocket League. */\nconst EPIC_APP_NAME = 'Sugar';\n\nexport interface EnsureStatsApiOptions {\n\t/** Rocket League install directory (the folder containing `TAGame`). Auto-detected on Windows when omitted. */\n\tinstallDir?: string;\n\t/** Exact path of the `.ini` file to check. Overrides `installDir` and auto-detection. */\n\tiniPath?: string;\n\t/** `PacketSendRate` written when the current value is `0` or less, missing, or above `120`. Must be 1–120. @default 30 */\n\tpacketSendRate?: number;\n\t/**\n\t * Called with a human-readable message when the file was changed (restart required) or could not be written.\n\t * Pass `false` to stay silent.\n\t * @default console.warn\n\t */\n\twarn?: ((message: string) => void) | false;\n}\n\nexport type EnsureStatsApiResult =\n\t/** No Stats API `.ini` file was found. */\n\t| { status: 'not-found'; searched: string[] }\n\t/** `PacketSendRate` was already between 1 and 120. */\n\t| { status: 'enabled'; iniPath: string; packetSendRate: number }\n\t/** `PacketSendRate` was `0` or less, missing, or above 120, and has been set. The game must be restarted. */\n\t| { status: 'updated'; iniPath: string; previous: number | undefined; packetSendRate: number; restartRequired: true }\n\t/** The file needed a change but could not be written (e.g. no permission under `Program Files`). */\n\t| { status: 'write-failed'; iniPath: string; previous: number | undefined; error: unknown };\n\n// ---------------------------------------------------------------------------\n// INI editing (pure)\n// ---------------------------------------------------------------------------\n\nexport interface PacketSendRateEdit {\n\t/** Current value, or `undefined` if the key is missing (the game then uses `0`). */\n\tprevious: number | undefined;\n\t/** `true` when `PacketSendRate` is `0` or less, missing (Stats API disabled), or above {@link MAX_PACKET_SEND_RATE}. */\n\tneedsUpdate: boolean;\n\t/** The file contents with `PacketSendRate` set to the requested rate. Equals the input when `needsUpdate` is false. */\n\tcontent: string;\n}\n\nconst SECTION_RE = /^\\s*\\[([^\\]]+)\\]\\s*$/;\nconst KEY_RE = /^(\\s*PacketSendRate\\s*=\\s*)([^;\\r\\n]*?)(\\s*(?:;.*)?)$/i;\n\n/**\n * Reads `PacketSendRate` from the `[TAGame.MatchStatsExporter_TA]` section of an `.ini` file\n * and, if it is `0` or less (or missing), returns the contents with it set to `rate`.\n * Keeps comments, the other keys, the BOM and the line endings untouched.\n */\nexport function editPacketSendRate(content: string, rate = DEFAULT_PACKET_SEND_RATE): PacketSendRateEdit {\n\tconst eol = content.includes('\\r\\n') ? '\\r\\n' : '\\n';\n\tconst lines = content.split(/\\r?\\n/);\n\n\tlet sectionStart = -1;\n\tlet sectionEnd = lines.length;\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst match = SECTION_RE.exec(lines[i]!);\n\t\tif (!match) continue;\n\t\tif (sectionStart !== -1) {\n\t\t\tsectionEnd = i;\n\t\t\tbreak;\n\t\t}\n\t\tif (match[1]!.trim().toLowerCase() === STATS_API_SECTION.toLowerCase()) sectionStart = i;\n\t}\n\n\tlet keyLine = -1;\n\tlet previous: number | undefined;\n\tif (sectionStart !== -1) {\n\t\tfor (let i = sectionStart + 1; i < sectionEnd; i++) {\n\t\t\tconst match = KEY_RE.exec(lines[i]!);\n\t\t\tif (match) {\n\t\t\t\tkeyLine = i;\n\t\t\t\tconst value = Number.parseFloat(match[2]!);\n\t\t\t\tprevious = Number.isNaN(value) ? undefined : value;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst needsUpdate = previous === undefined || previous <= 0 || previous > MAX_PACKET_SEND_RATE;\n\tif (!needsUpdate) return { previous, needsUpdate, content };\n\n\tif (keyLine !== -1) {\n\t\tlines[keyLine] = lines[keyLine]!.replace(KEY_RE, (_, prefix: string, _value: string, suffix: string) => `${prefix}${rate}${suffix}`);\n\t} else if (sectionStart !== -1) {\n\t\tlines.splice(sectionStart + 1, 0, `PacketSendRate=${rate}`);\n\t} else {\n\t\t// Append the section, keeping a trailing newline if the file had one.\n\t\tconst hasTrailingNewline = lines.length > 0 && lines.at(-1) === '';\n\t\tif (hasTrailingNewline) lines.pop();\n\t\tif (lines.length > 0 && lines.at(-1)!.trim() !== '') lines.push('');\n\t\tlines.push(`[${STATS_API_SECTION}]`, `PacketSendRate=${rate}`);\n\t\tif (hasTrailingNewline) lines.push('');\n\t}\n\treturn { previous, needsUpdate, content: lines.join(eol) };\n}\n\n// ---------------------------------------------------------------------------\n// Install directory discovery\n// ---------------------------------------------------------------------------\n\nasync function exists(file: string): Promise<boolean> {\n\ttry {\n\t\tawait access(file);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nasync function readJson(file: string): Promise<unknown> {\n\ttry {\n\t\treturn JSON.parse(await readFile(file, 'utf8'));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction epicInstallLocation(value: unknown): string | undefined {\n\tif (typeof value !== 'object' || value === null) return undefined;\n\tconst { AppName, InstallLocation } = value as Record<string, unknown>;\n\treturn AppName === EPIC_APP_NAME && typeof InstallLocation === 'string' ? InstallLocation : undefined;\n}\n\n/** Install directories registered by the Epic Games Launcher. */\nasync function findEpicInstalls(programData: string): Promise<string[]> {\n\tconst found: string[] = [];\n\n\tconst launcherInstalled = await readJson(path.join(programData, 'Epic', 'UnrealEngineLauncher', 'LauncherInstalled.dat'));\n\tconst list = (launcherInstalled as { InstallationList?: unknown } | undefined)?.InstallationList;\n\tif (Array.isArray(list)) {\n\t\tfor (const item of list) {\n\t\t\tconst location = epicInstallLocation(item);\n\t\t\tif (location) found.push(location);\n\t\t}\n\t}\n\n\tconst manifestsDir = path.join(programData, 'Epic', 'EpicGamesLauncher', 'Data', 'Manifests');\n\ttry {\n\t\tfor (const file of await readdir(manifestsDir)) {\n\t\t\tif (!file.toLowerCase().endsWith('.item')) continue;\n\t\t\tconst location = epicInstallLocation(await readJson(path.join(manifestsDir, file)));\n\t\t\tif (location) found.push(location);\n\t\t}\n\t} catch {\n\t\t// No Epic launcher.\n\t}\n\treturn found;\n}\n\n/** Install directories in every Steam library. */\nasync function findSteamInstalls(steamDir: string): Promise<string[]> {\n\tconst libraries = [steamDir];\n\ttry {\n\t\tconst vdf = await readFile(path.join(steamDir, 'steamapps', 'libraryfolders.vdf'), 'utf8');\n\t\tfor (const match of vdf.matchAll(/\"path\"\\s+\"([^\"]+)\"/g)) libraries.push(match[1]!.replace(/\\\\\\\\/g, '\\\\'));\n\t} catch {\n\t\t// No Steam, or no extra libraries.\n\t}\n\treturn libraries.map((library) => path.join(library, 'steamapps', 'common', 'rocketleague'));\n}\n\n/**\n * Candidate Rocket League install directories (Epic Games Store and Steam, Windows only).\n * Not every returned directory necessarily exists.\n */\nexport async function findRocketLeagueInstallDirs(): Promise<string[]> {\n\tif (process.platform !== 'win32') return [];\n\tconst env = process.env;\n\tconst programData = env.ProgramData ?? 'C:\\\\ProgramData';\n\tconst programFiles = env.ProgramFiles ?? 'C:\\\\Program Files';\n\tconst programFilesX86 = env['ProgramFiles(x86)'] ?? 'C:\\\\Program Files (x86)';\n\n\tconst candidates = [\n\t\t...(await findEpicInstalls(programData)),\n\t\tpath.join(programFiles, 'Epic Games', 'rocketleague'),\n\t\t...(await findSteamInstalls(path.join(programFilesX86, 'Steam'))),\n\t];\n\treturn [...new Set(candidates.map((dir) => path.normalize(dir)))];\n}\n\n/** Paths of the Stats API `.ini` files inside an install directory, in lookup order. */\nexport function statsApiIniPaths(installDir: string): string[] {\n\treturn STATS_API_INI_FILES.map((file) => path.join(installDir, 'TAGame', 'Config', file));\n}\n\n/** Returns the Stats API `.ini` file in use (`TAStatsAPI.ini`, else `DefaultStatsAPI.ini`), or `undefined`. */\nexport async function findStatsApiIni(installDir?: string): Promise<{ iniPath: string | undefined; searched: string[] }> {\n\tconst dirs = installDir ? [installDir] : await findRocketLeagueInstallDirs();\n\tconst searched: string[] = [];\n\tfor (const dir of dirs) {\n\t\tfor (const file of statsApiIniPaths(dir)) {\n\t\t\tsearched.push(file);\n\t\t\tif (await exists(file)) return { iniPath: file, searched };\n\t\t}\n\t}\n\treturn { iniPath: undefined, searched };\n}\n\n/** Why `PacketSendRate` needs to be changed, for warnings. */\nfunction describeProblem(previous: number | undefined): string {\n\treturn previous !== undefined && previous > MAX_PACKET_SEND_RATE\n\t\t? `PacketSendRate=${previous} is above the maximum of ${MAX_PACKET_SEND_RATE}`\n\t\t: `The Rocket League Stats API is disabled (PacketSendRate=${previous ?? 0})`;\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Looks for the Stats API `.ini` file (`TAStatsAPI.ini` or `DefaultStatsAPI.ini`). If `PacketSendRate`\n * is `0` or less, missing, or above `120`, sets it to `30` (or `options.packetSendRate`) and warns that the game must be restarted,\n * since Rocket League only reads this file at launch.\n *\n * Never throws: every outcome is reported in the returned result.\n */\nexport async function ensureStatsApiEnabled(options: EnsureStatsApiOptions = {}): Promise<EnsureStatsApiResult> {\n\tconst rate = options.packetSendRate ?? DEFAULT_PACKET_SEND_RATE;\n\tif (!Number.isFinite(rate) || rate <= 0 || rate > MAX_PACKET_SEND_RATE) {\n\t\tthrow new RangeError(`packetSendRate must be between 1 and ${MAX_PACKET_SEND_RATE}, got ${rate}.`);\n\t}\n\tconst warn = options.warn === false ? () => {} : (options.warn ?? ((message: string) => console.warn(message)));\n\n\tlet iniPath = options.iniPath;\n\tlet searched = iniPath ? [iniPath] : [];\n\tif (iniPath === undefined) {\n\t\t({ iniPath, searched } = await findStatsApiIni(options.installDir));\n\t}\n\n\tlet content: string;\n\tlet encoding: BufferEncoding;\n\ttry {\n\t\tif (!iniPath) throw new Error('not found');\n\t\tconst buffer = await readFile(iniPath);\n\t\t// Unreal may save .ini files as UTF-16LE (with BOM). Keep whatever encoding the file uses.\n\t\tencoding = buffer[0] === 0xff && buffer[1] === 0xfe ? 'utf16le' : 'utf8';\n\t\tcontent = buffer.toString(encoding);\n\t} catch {\n\t\treturn { status: 'not-found', searched };\n\t}\n\n\tconst edit = editPacketSendRate(content, rate);\n\tif (!edit.needsUpdate) return { status: 'enabled', iniPath, packetSendRate: edit.previous! };\n\n\ttry {\n\t\tawait writeFile(iniPath, edit.content, encoding);\n\t} catch (error) {\n\t\twarn(\n\t\t\t`[@juande_pl/rocketleague-stats-api-client] ${describeProblem(edit.previous)}, ` +\n\t\t\t\t`but \"${iniPath}\" could not be written. Set PacketSendRate=${rate} manually (you may need administrator rights) ` +\n\t\t\t\t'and restart the game.',\n\t\t);\n\t\treturn { status: 'write-failed', iniPath, previous: edit.previous, error };\n\t}\n\n\twarn(\n\t\t`[@juande_pl/rocketleague-stats-api-client] ${describeProblem(edit.previous)}. ` +\n\t\t\t`PacketSendRate has been set to ${rate} in \"${iniPath}\". Restart Rocket League for the Stats API to work.`,\n\t);\n\treturn { status: 'updated', iniPath, previous: edit.previous, packetSendRate: rate, restartRequired: true };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PACKET_SEND_RATE,
|
|
3
|
+
MAX_PACKET_SEND_RATE,
|
|
4
|
+
STATS_API_INI_FILES,
|
|
5
|
+
STATS_API_SECTION,
|
|
6
|
+
editPacketSendRate,
|
|
7
|
+
ensureStatsApiEnabled,
|
|
8
|
+
findRocketLeagueInstallDirs,
|
|
9
|
+
findStatsApiIni,
|
|
10
|
+
statsApiIniPaths
|
|
11
|
+
} from "./chunk-IUPCJADJ.js";
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_PACKET_SEND_RATE,
|
|
14
|
+
MAX_PACKET_SEND_RATE,
|
|
15
|
+
STATS_API_INI_FILES,
|
|
16
|
+
STATS_API_SECTION,
|
|
17
|
+
editPacketSendRate,
|
|
18
|
+
ensureStatsApiEnabled,
|
|
19
|
+
findRocketLeagueInstallDirs,
|
|
20
|
+
findStatsApiIni,
|
|
21
|
+
statsApiIniPaths
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=config-3VSG3YBY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|