@juande_pl/rocketleague-stats-api-client 1.0.0 → 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/README.md CHANGED
@@ -1,177 +1,204 @@
1
- # rocketleague-stats-api-client
2
-
3
- Fully typed WebSocket client for the official [Rocket League Stats API](https://www.rocketleague.com/developer/stats-api), with runtime type guards for every event and command.
4
-
5
- - Typed interfaces for all 22 events and 6 commands
6
- - Type guards that validate each message at runtime (enabled by default)
7
- - `RocketLeagueStatsClient`: connects automatically, reconnects with backoff, and gives you typed `on()` / `onAny()` / `send()`
8
- - No runtime dependencies. Works in Node.js >= 22 and in browsers. ESM and CommonJS builds.
9
-
10
- ## Install
11
-
12
- ```sh
13
- npm install @juande_pl/rocketleague-stats-api-client
14
- ```
15
-
16
- ## Enable the Stats API in Rocket League
17
-
18
- Edit `<Install Dir>\TAGame\Config\TAStatsAPI.ini` (or `DefaultStatsAPI.ini` if it doesn't exist) **before launching the game**:
19
-
20
- ```ini
21
- [TAGame.MatchStatsExporter_TA]
22
- PacketSendRate=30
23
- WebPort=49124
24
- ```
25
-
26
- | Setting | Default | Description |
27
- | ---------------- | ------- | ------------------------------------------------------------------- |
28
- | `PacketSendRate` | `0` | `UpdateState` packets per second (max 120). `0` disables the feature. |
29
- | `Port` | `49123` | TCP port. `0` disables it. |
30
- | `WebPort` | `49124` | WebSocket port used by this library. `0` disables it. |
31
-
32
- ### Automatic setup (Node.js)
33
-
34
- On Node.js the client checks the `.ini` for you. It looks for `TAStatsAPI.ini` first, then `DefaultStatsAPI.ini`. On Windows it finds the Epic Games and Steam installs by itself. If `PacketSendRate` is `0` or less, missing, or above the maximum of `120`, it sets it to `30` and warns that **you must restart the game** for the Stats API to work. The game only reads this file at launch. In browsers the check is skipped and the Node-only code is never loaded.
35
-
36
- ```ts
37
- import { RocketLeagueStatsClient } from '@juande_pl/rocketleague-stats-api-client';
38
-
39
- const client = new RocketLeagueStatsClient({
40
- ensureStatsApi: true, // default. Or false, or { installDir, iniPath, packetSendRate, warn }
41
- });
42
-
43
- const config = await client.statsApiConfig;
44
- // { status: 'updated', iniPath, previous: 0, packetSendRate: 30, restartRequired: true }
45
- // or 'enabled' | 'not-found' | 'write-failed' (e.g. no permission under Program Files)
46
- ```
47
-
48
- The helpers are exported from `@juande_pl/rocketleague-stats-api-client/node`. Run the check on its own with `ensureStatsApiEnabled(options)`, or use `editPacketSendRate(content)` to edit the `.ini` text without touching the disk.
49
-
50
- ## Usage
51
-
52
- ```ts
53
- import { RocketLeagueStatsClient } from "@juande_pl/rocketleague-stats-api-client";
54
-
55
- const client = new RocketLeagueStatsClient(); // ws://127.0.0.1:49124, connects automatically
56
-
57
- // A single event: `data` is typed as GoalScoredData
58
- client.on('GoalScored', (data) => {
59
- console.log(`${data.Scorer.Name} scored at ${data.GoalSpeed} uu/s`);
60
- });
61
-
62
- // Every event, as a discriminated union
63
- client.onAny((event) => {
64
- if (event.Event === 'StatfeedEvent') {
65
- console.log(event.Data.MainTarget.Name, event.Data.EventName);
66
- }
67
- });
68
-
69
- // Commands: checked at compile time and validated at runtime
70
- client.send('ChangePOV', { Focus: '1', Perspective: 'PlayerView' });
71
- client.send({ Command: 'SetHUDVisibility', Data: { bVisible: false } });
72
- ```
73
-
74
- ### Options
75
-
76
- ```ts
77
- new RocketLeagueStatsClient({
78
- host: '127.0.0.1', // default
79
- port: 49124, // default (WebPort)
80
- secure: false, // use wss://
81
- url: undefined, // full URL, overrides host/port/secure
82
- autoConnect: true, // set to false and call client.connect() yourself
83
- reconnect: true, // or false, or { initialDelay, maxDelay, factor, maxAttempts }
84
- validateEvents: true, // run type guards on incoming events
85
- WebSocket: undefined, // custom implementation, defaults to globalThis.WebSocket
86
- ensureStatsApi: true, // Node.js only: enable PacketSendRate in the .ini (see above)
87
- });
88
- ```
89
-
90
- With `autoConnect: true`, the connection opens on the next microtask, so listeners you add right after `new` still get the `connect` event. If the game isn't running yet, the client keeps retrying with exponential backoff (1 s, 2 s, 4 s … up to 30 s).
91
-
92
- On Node.js < 22, pass the `ws` package: `new RocketLeagueStatsClient({ WebSocket: (await import('ws')).default })`.
93
-
94
- ### Listening
95
-
96
- | Method | Description |
97
- | ------------------------------- | --------------------------------------------------------------------------- |
98
- | `on(name, listener)` | Game event (`'GoalScored'`, …) or lifecycle event. Returns an unsubscribe function. |
99
- | `once(name, listener)` | Same as `on`, but the listener runs only once. |
100
- | `off(name, listener)` | Removes a listener. |
101
- | `onAny(listener)` / `offAny()` | Every game event, as `StatsApiEvent` (narrow with `event.Event`). |
102
- | `waitFor(name, { timeout })` | Promise that resolves with the next payload of that event. |
103
- | `removeAllListeners(name?)` | Removes every listener, or every listener for one name. |
104
-
105
- Game event listeners receive `(data, event)`: the typed payload and the full `{ Event, Data }` message.
106
-
107
- Lifecycle events:
108
-
109
- | Name | Arguments |
110
- | ---------------- | ------------------------------------------- |
111
- | `connect` | none |
112
- | `disconnect` | `{ code, reason, willReconnect }` |
113
- | `reconnecting` | `{ attempt, delay }` |
114
- | `error` | `Error` (`ConnectionError`, `ListenerError`) |
115
- | `invalidMessage` | `ParseFailure` (`reason`, `raw`, `event?`, `data?`) |
116
-
117
- Errors are only reported to `error` listeners. A throwing listener doesn't stop the other listeners from running.
118
-
119
- ### Sending commands
120
-
121
- `send(command, data)` or `send({ Command, Data })`. It throws:
122
-
123
- - `CommandValidationError` if the command name or payload is invalid (for example, `ChangePOV` with neither `Focus` nor `Perspective`, or `SetGameSpeed` with a negative speed).
124
- - `NotConnectedError` if the socket is not open.
125
-
126
- | Command | Payload |
127
- | ------------------ | --------------------------------------------------------------- |
128
- | `ChangePOV` | `{ Focus?: 'Ball' \| '<shortcut>', Perspective?: Perspective }` (at least one) |
129
- | `LoadReplay` | `{ FileName?: string, Path?: string }` (at least one) |
130
- | `SeekReplay` | `{ Frame?: number, TimeSeconds?: number }` (at least one) |
131
- | `SetGameSpeed` | `{ Speed: number }` (≥ 0) |
132
- | `SetHUDVisibility` | `{ bVisible: boolean }` |
133
- | `SetMatchPaused` | `{ bPaused: boolean }` |
134
-
135
- `Perspective` is one of `'Fly' | 'SoftAttach' | 'HardAttach' | 'PlayerView' | 'AutoCam' | 'Camera_Director'`.
136
-
137
- ## Type guards
138
-
139
- Every guard is exported, so you can validate messages from any source (the TCP port, recordings, a relay server…):
140
-
141
- ```ts
142
- import { isStatsApiEvent, isGoalScoredData, isEventData, isStatsApiCommand, parseEventMessage } from '@juande_pl/rocketleague-stats-api-client';
143
-
144
- isGoalScoredData(payload); // payload is GoalScoredData
145
- isEventData('BallHit', payload); // payload is BallHitData
146
- isStatsApiEvent(message); // message is StatsApiEvent
147
- isStatsApiEvent(message, 'MatchEnded'); // message is StatsApiEventOf<'MatchEnded'>
148
- isStatsApiCommand(command, 'ChangePOV'); // command is StatsApiCommandOf<'ChangePOV'>
149
-
150
- // Decodes the wire format ({ Event, Data: "<json string>" }). Never throws.
151
- const result = parseEventMessage(rawText);
152
- if (result.ok) console.log(result.event.Event);
153
- else console.warn(result.reason); // 'invalid-json' | 'invalid-envelope' | 'invalid-data-json' | 'unknown-event' | 'invalid-data'
154
- ```
155
-
156
- Guards accept extra, undocumented fields, so new fields added by a game update don't break validation. Fields the docs mark `SPECTATOR` or `CONDITIONAL` (and `MatchGuid`, which is only set online or on LAN) are optional.
157
-
158
- The building blocks (`shape`, `optional`, `arrayOf`, `oneOf`, `isString`, `isNumber`, …) are exported too.
159
-
160
- ## Known quirks
161
-
162
- - `StatfeedEvent.Type` is a localized label. Use `EventName` in your logic.
163
- - `GoalScored.GoalTime` is the length of the previous round, not the match clock.
164
- - In offline exhibitions or with splitscreen players, `StatfeedEvent` can arrive more than once.
165
-
166
- ## Development
167
-
168
- ```sh
169
- npm install
170
- npm run typecheck
171
- npm test
172
- npm run build
173
- ```
174
-
175
- ## License
176
-
177
- MIT. This project is not affiliated with Psyonix or Epic Games.
1
+ # rocketleague-stats-api-client
2
+
3
+ Fully typed WebSocket client for the official [Rocket League Stats API](https://www.rocketleague.com/developer/stats-api), with runtime type guards for every event and command.
4
+
5
+ - Typed interfaces for all 22 events and 6 commands
6
+ - Type guards that validate each message at runtime (enabled by default)
7
+ - `RocketLeagueStatsClient`: connects automatically, reconnects with backoff, and gives you typed `on()` / `onAny()` / `send()`
8
+ - No runtime dependencies. Works in Node.js >= 22 and in browsers. ESM and CommonJS builds.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @juande_pl/rocketleague-stats-api-client
14
+ ```
15
+
16
+ ## Enable the Stats API in Rocket League
17
+
18
+ Edit `<Install Dir>\TAGame\Config\TAStatsAPI.ini` (or `DefaultStatsAPI.ini` if it doesn't exist) **before launching the game**:
19
+
20
+ ```ini
21
+ [TAGame.MatchStatsExporter_TA]
22
+ PacketSendRate=30
23
+ WebPort=49124
24
+ ```
25
+
26
+ | Setting | Default | Description |
27
+ | ---------------- | ------- | ------------------------------------------------------------------- |
28
+ | `PacketSendRate` | `0` | `UpdateState` packets per second (max 120). `0` disables the feature. |
29
+ | `Port` | `49123` | TCP port. `0` disables it. |
30
+ | `WebPort` | `49124` | WebSocket port used by this library. `0` disables it. |
31
+
32
+ ### Automatic setup (Node.js)
33
+
34
+ On Node.js the client checks the `.ini` for you. It looks for `TAStatsAPI.ini` first, then `DefaultStatsAPI.ini`. On Windows it finds the Epic Games and Steam installs by itself. If `PacketSendRate` is `0` or less, missing, or above the maximum of `120`, it sets it to `30` and warns that **you must restart the game** for the Stats API to work. The game only reads this file at launch. In browsers the check is skipped and the Node-only code is never loaded.
35
+
36
+ ```ts
37
+ import { RocketLeagueStatsClient } from '@juande_pl/rocketleague-stats-api-client';
38
+
39
+ const client = new RocketLeagueStatsClient({
40
+ ensureStatsApi: true, // default. Or false, or { installDir, iniPath, packetSendRate, warn }
41
+ });
42
+
43
+ const config = await client.statsApiConfig;
44
+ // { status: 'updated', iniPath, previous: 0, packetSendRate: 30, restartRequired: true }
45
+ // or 'enabled' | 'not-found' | 'write-failed' (e.g. no permission under Program Files)
46
+ ```
47
+
48
+ The helpers are exported from `@juande_pl/rocketleague-stats-api-client/node`. Run the check on its own with `ensureStatsApiEnabled(options)`, or use `editPacketSendRate(content)` to edit the `.ini` text without touching the disk.
49
+
50
+ ## Usage
51
+
52
+ ```ts
53
+ import { RocketLeagueStatsClient } from "@juande_pl/rocketleague-stats-api-client";
54
+
55
+ const client = new RocketLeagueStatsClient(); // ws://127.0.0.1:49124, connects automatically
56
+
57
+ // A single event: `data` is typed as GoalScoredData
58
+ client.on('GoalScored', (data) => {
59
+ console.log(`${data.Scorer.Name} scored at ${data.GoalSpeed} uu/s`);
60
+ });
61
+
62
+ // Every event, as a discriminated union
63
+ client.onAny((event) => {
64
+ if (event.Event === 'StatfeedEvent') {
65
+ console.log(event.Data.MainTarget.Name, event.Data.EventName);
66
+ }
67
+ });
68
+
69
+ // Commands: checked at compile time and validated at runtime
70
+ client.send('ChangePOV', { Focus: '1', Perspective: 'PlayerView' });
71
+ client.send({ Command: 'SetHUDVisibility', Data: { bVisible: false } });
72
+ ```
73
+
74
+ ### Options
75
+
76
+ ```ts
77
+ new RocketLeagueStatsClient({
78
+ host: '127.0.0.1', // default
79
+ port: 49124, // default (WebPort)
80
+ secure: false, // use wss://
81
+ url: undefined, // full URL, overrides host/port/secure
82
+ autoConnect: true, // set to false and call client.connect() yourself
83
+ reconnect: true, // or false, or { initialDelay, maxDelay, factor, maxAttempts }
84
+ validateEvents: true, // run type guards on incoming events
85
+ ignoreReplayEndGoal: true, // drop the fake GoalScored sent when a replay ends
86
+ sortPlayers: undefined, // sort UpdateState.Players (see below). Default: the game's order
87
+ WebSocket: undefined, // custom implementation, defaults to globalThis.WebSocket
88
+ ensureStatsApi: true, // Node.js only: enable PacketSendRate in the .ini (see above)
89
+ });
90
+ ```
91
+
92
+ With `autoConnect: true`, the connection opens on the next microtask, so listeners you add right after `new` still get the `connect` event. If the game isn't running yet, the client keeps retrying with exponential backoff (1 s, 2 s, 4 s … up to 30 s).
93
+
94
+ On Node.js < 22, pass the `ws` package: `new RocketLeagueStatsClient({ WebSocket: (await import('ws')).default })`.
95
+
96
+ ### Listening
97
+
98
+ | Method | Description |
99
+ | ------------------------------- | --------------------------------------------------------------------------- |
100
+ | `on(name, listener)` | Game event (`'GoalScored'`, …) or lifecycle event. Returns an unsubscribe function. |
101
+ | `once(name, listener)` | Same as `on`, but the listener runs only once. |
102
+ | `off(name, listener)` | Removes a listener. |
103
+ | `onAny(listener)` / `offAny()` | Every game event, as `StatsApiEvent` (narrow with `event.Event`). |
104
+ | `waitFor(name, { timeout })` | Promise that resolves with the next payload of that event. |
105
+ | `removeAllListeners(name?)` | Removes every listener, or every listener for one name. |
106
+
107
+ Game event listeners receive `(data, event)`: the typed payload and the full `{ Event, Data }` message.
108
+
109
+ Lifecycle events:
110
+
111
+ | Name | Arguments |
112
+ | ---------------- | ------------------------------------------- |
113
+ | `connect` | none |
114
+ | `disconnect` | `{ code, reason, willReconnect }` |
115
+ | `reconnecting` | `{ attempt, delay }` |
116
+ | `error` | `Error` (`ConnectionError`, `ListenerError`) |
117
+ | `invalidMessage` | `ParseFailure` (`reason`, `raw`, `event?`, `data?`) |
118
+
119
+ Errors are only reported to `error` listeners. A throwing listener doesn't stop the other listeners from running.
120
+
121
+ ### Sending commands
122
+
123
+ `send(command, data)` or `send({ Command, Data })`. It throws:
124
+
125
+ - `CommandValidationError` if the command name or payload is invalid (for example, `ChangePOV` with neither `Focus` nor `Perspective`, or `SetGameSpeed` with a negative speed).
126
+ - `NotConnectedError` if the socket is not open.
127
+
128
+ | Command | Payload |
129
+ | ------------------ | --------------------------------------------------------------- |
130
+ | `ChangePOV` | `{ Focus?: 'Ball' \| '<shortcut>', Perspective?: Perspective }` (at least one) |
131
+ | `LoadReplay` | `{ FileName?: string, Path?: string }` (at least one) |
132
+ | `SeekReplay` | `{ Frame?: number, TimeSeconds?: number }` (at least one) |
133
+ | `SetGameSpeed` | `{ Speed: number }` (≥ 0) |
134
+ | `SetHUDVisibility` | `{ bVisible: boolean }` |
135
+ | `SetMatchPaused` | `{ bPaused: boolean }` |
136
+
137
+ `Perspective` is one of `'Fly' | 'SoftAttach' | 'HardAttach' | 'PlayerView' | 'AutoCam' | 'Camera_Director'`.
138
+
139
+ ## Sorting players
140
+
141
+ By default `UpdateState.Players` arrives in the order the game sends it. To change that, pass `sortPlayers` to the client, or call `sortPlayers()` yourself on any player list:
142
+
143
+ ```ts
144
+ import { RocketLeagueStatsClient, sortPlayers, byTeam, byStat, byName, byShortcut } from '@juande_pl/rocketleague-stats-api-client';
145
+
146
+ // Every UpdateState: Blue then Orange, best score first within each team
147
+ const client = new RocketLeagueStatsClient({ sortPlayers: [byTeam(), byStat('Score')] });
148
+
149
+ // Or on demand. Returns a new array and doesn't change the original.
150
+ const byBoost = sortPlayers(data.Players, byStat('Boost'));
151
+ const custom = sortPlayers(data.Players, (a, b) => a.Demos - b.Demos);
152
+ ```
153
+
154
+ | Comparator | Order |
155
+ | ----------------------------- | ------------------------------------------------------------------ |
156
+ | `byTeam(order = 'asc')` | Blue (0) first. `'desc'` puts Orange first. |
157
+ | `byShortcut(order = 'asc')` | Spectator shortcut. |
158
+ | `byName(order = 'asc')` | Name, case-insensitive. |
159
+ | `byStat(stat, order = 'desc')`| Any numeric field (`Score`, `Goals`, `Boost`…). Missing values go last. |
160
+
161
+ The first comparator decides and the next ones break ties. The sort is stable, so players that compare equal keep the game's order.
162
+
163
+ ## Type guards
164
+
165
+ Every guard is exported, so you can validate messages from any source (the TCP port, recordings, a relay server…):
166
+
167
+ ```ts
168
+ import { isStatsApiEvent, isGoalScoredData, isEventData, isStatsApiCommand, parseEventMessage } from '@juande_pl/rocketleague-stats-api-client';
169
+
170
+ isGoalScoredData(payload); // payload is GoalScoredData
171
+ isEventData('BallHit', payload); // payload is BallHitData
172
+ isStatsApiEvent(message); // message is StatsApiEvent
173
+ isStatsApiEvent(message, 'MatchEnded'); // message is StatsApiEventOf<'MatchEnded'>
174
+ isStatsApiCommand(command, 'ChangePOV'); // command is StatsApiCommandOf<'ChangePOV'>
175
+
176
+ // Decodes the wire format ({ Event, Data: "<json string>" }). Never throws.
177
+ const result = parseEventMessage(rawText);
178
+ if (result.ok) console.log(result.event.Event);
179
+ else console.warn(result.reason); // 'invalid-json' | 'invalid-envelope' | 'invalid-data-json' | 'unknown-event' | 'invalid-data'
180
+ ```
181
+
182
+ Guards accept extra, undocumented fields, so new fields added by a game update don't break validation. Fields the docs mark `SPECTATOR` or `CONDITIONAL` (and `MatchGuid`, which is only set online or on LAN) are optional.
183
+
184
+ The building blocks (`shape`, `optional`, `arrayOf`, `oneOf`, `isString`, `isNumber`, …) are exported too.
185
+
186
+ ## Known quirks
187
+
188
+ - `StatfeedEvent.Type` is a localized label. Use `EventName` in your logic.
189
+ - `GoalScored.GoalTime` is the length of the previous round, not the match clock.
190
+ - In offline exhibitions or with splitscreen players, `StatfeedEvent` can arrive more than once.
191
+ - When a replay ends, the game sends a fake `GoalScored` event whose `Scorer` is `{ "Name": "", "Shortcut": 0, "TeamNum": 0 }`. The client drops it by default. Pass `ignoreReplayEndGoal: false` to receive it, and use `isReplayEndGoal(event)` to detect it yourself.
192
+
193
+ ## Development
194
+
195
+ ```sh
196
+ npm install
197
+ npm run typecheck
198
+ npm test
199
+ npm run build
200
+ ```
201
+
202
+ ## License
203
+
204
+ MIT. This project is not affiliated with Psyonix or Epic Games.
@@ -333,6 +333,37 @@ function parseEventMessage(raw, options = {}) {
333
333
  return { ok: true, event: { Event: event, Data: data } };
334
334
  }
335
335
 
336
+ // src/sort.ts
337
+ var direction = (order) => order === "asc" ? 1 : -1;
338
+ function byTeam(order = "asc") {
339
+ return (a, b) => (a.TeamNum - b.TeamNum) * direction(order);
340
+ }
341
+ function byShortcut(order = "asc") {
342
+ return (a, b) => (a.Shortcut - b.Shortcut) * direction(order);
343
+ }
344
+ function byName(order = "asc") {
345
+ return (a, b) => a.Name.localeCompare(b.Name, void 0, { sensitivity: "base" }) * direction(order);
346
+ }
347
+ function byStat(stat, order = "desc") {
348
+ return (a, b) => {
349
+ const x = a[stat];
350
+ const y = b[stat];
351
+ if (x === void 0 || y === void 0) return x === y ? 0 : x === void 0 ? 1 : -1;
352
+ return (x - y) * direction(order);
353
+ };
354
+ }
355
+ function sortPlayers(players, ...comparators) {
356
+ const copy = [...players];
357
+ if (comparators.length === 0) return copy;
358
+ return copy.sort((a, b) => {
359
+ for (const compare of comparators) {
360
+ const result = compare(a, b);
361
+ if (result !== 0) return result;
362
+ }
363
+ return 0;
364
+ });
365
+ }
366
+
336
367
  // src/errors.ts
337
368
  var StatsApiError = class extends Error {
338
369
  name = "StatsApiError";
@@ -367,6 +398,11 @@ var ListenerError = class extends StatsApiError {
367
398
 
368
399
  // src/client.ts
369
400
  var WS_OPEN = 1;
401
+ function isReplayEndGoal(event) {
402
+ if (event.Event !== "GoalScored") return false;
403
+ const { Name, Shortcut, TeamNum } = event.Data.Scorer;
404
+ return Name === "" && Shortcut === 0 && TeamNum === 0;
405
+ }
370
406
  function isNodeRuntime() {
371
407
  const proc = globalThis.process;
372
408
  return typeof proc?.versions?.node === "string";
@@ -382,6 +418,8 @@ var RocketLeagueStatsClient = class {
382
418
  #autoConnect;
383
419
  #reconnect;
384
420
  #validateEvents;
421
+ #ignoreReplayEndGoal;
422
+ #playerComparators;
385
423
  #WebSocket;
386
424
  #socket = null;
387
425
  #state = "idle";
@@ -395,6 +433,8 @@ var RocketLeagueStatsClient = class {
395
433
  this.url = options.url ?? `${protocol}://${options.host ?? "127.0.0.1"}:${options.port ?? 49124}`;
396
434
  this.#autoConnect = options.autoConnect ?? true;
397
435
  this.#validateEvents = options.validateEvents ?? true;
436
+ this.#ignoreReplayEndGoal = options.ignoreReplayEndGoal ?? true;
437
+ this.#playerComparators = options.sortPlayers === void 0 ? [] : [options.sortPlayers].flat();
398
438
  this.#WebSocket = options.WebSocket ?? globalThis.WebSocket;
399
439
  const reconnect = options.reconnect ?? true;
400
440
  this.#reconnect = reconnect === false ? null : {
@@ -533,6 +573,10 @@ var RocketLeagueStatsClient = class {
533
573
  return;
534
574
  }
535
575
  const { event } = result;
576
+ if (this.#ignoreReplayEndGoal && isReplayEndGoal(event)) return;
577
+ if (this.#playerComparators.length > 0 && event.Event === "UpdateState" && Array.isArray(event.Data.Players)) {
578
+ event.Data.Players = sortPlayers(event.Data.Players, ...this.#playerComparators);
579
+ }
536
580
  this.#dispatch(event.Event, [event.Data, event]);
537
581
  for (const listener of [...this.#anyListeners]) {
538
582
  this.#invoke("*", listener, [event]);
@@ -716,11 +760,17 @@ export {
716
760
  isCommandData,
717
761
  isStatsApiCommand,
718
762
  parseEventMessage,
763
+ byTeam,
764
+ byShortcut,
765
+ byName,
766
+ byStat,
767
+ sortPlayers,
719
768
  StatsApiError,
720
769
  NotConnectedError,
721
770
  CommandValidationError,
722
771
  ConnectionError,
723
772
  ListenerError,
773
+ isReplayEndGoal,
724
774
  RocketLeagueStatsClient
725
775
  };
726
- //# sourceMappingURL=chunk-IVUQE3LJ.js.map
776
+ //# sourceMappingURL=chunk-5ZC7WJPK.js.map