@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JuandePL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,204 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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.
|