@juande_pl/rocketleague-stats-api-client 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +176 -2
- package/dist/chunk-IUPCJADJ.js +182 -0
- package/dist/chunk-IUPCJADJ.js.map +1 -0
- package/dist/chunk-IVUQE3LJ.js +726 -0
- package/dist/chunk-IVUQE3LJ.js.map +1 -0
- package/dist/config-3VSG3YBY.js +23 -0
- package/dist/config-3VSG3YBY.js.map +1 -0
- package/dist/index.cjs +1026 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +918 -0
- package/dist/index.d.ts +918 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/dist/node.cjs +1047 -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 +162 -0
- package/dist/node.js.map +1 -0
- package/package.json +78 -6
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res, err) => function __init() {
|
|
9
|
+
if (err) throw err[0];
|
|
10
|
+
try {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
throw err = [e], e;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
|
|
38
|
+
// src/node/config.ts
|
|
39
|
+
var config_exports = {};
|
|
40
|
+
__export(config_exports, {
|
|
41
|
+
DEFAULT_PACKET_SEND_RATE: () => DEFAULT_PACKET_SEND_RATE,
|
|
42
|
+
MAX_PACKET_SEND_RATE: () => MAX_PACKET_SEND_RATE,
|
|
43
|
+
STATS_API_INI_FILES: () => STATS_API_INI_FILES,
|
|
44
|
+
STATS_API_SECTION: () => STATS_API_SECTION,
|
|
45
|
+
editPacketSendRate: () => editPacketSendRate,
|
|
46
|
+
ensureStatsApiEnabled: () => ensureStatsApiEnabled,
|
|
47
|
+
findRocketLeagueInstallDirs: () => findRocketLeagueInstallDirs,
|
|
48
|
+
findStatsApiIni: () => findStatsApiIni,
|
|
49
|
+
statsApiIniPaths: () => statsApiIniPaths
|
|
50
|
+
});
|
|
51
|
+
function editPacketSendRate(content, rate = DEFAULT_PACKET_SEND_RATE) {
|
|
52
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
53
|
+
const lines = content.split(/\r?\n/);
|
|
54
|
+
let sectionStart = -1;
|
|
55
|
+
let sectionEnd = lines.length;
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const match = SECTION_RE.exec(lines[i]);
|
|
58
|
+
if (!match) continue;
|
|
59
|
+
if (sectionStart !== -1) {
|
|
60
|
+
sectionEnd = i;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (match[1].trim().toLowerCase() === STATS_API_SECTION.toLowerCase()) sectionStart = i;
|
|
64
|
+
}
|
|
65
|
+
let keyLine = -1;
|
|
66
|
+
let previous;
|
|
67
|
+
if (sectionStart !== -1) {
|
|
68
|
+
for (let i = sectionStart + 1; i < sectionEnd; i++) {
|
|
69
|
+
const match = KEY_RE.exec(lines[i]);
|
|
70
|
+
if (match) {
|
|
71
|
+
keyLine = i;
|
|
72
|
+
const value = Number.parseFloat(match[2]);
|
|
73
|
+
previous = Number.isNaN(value) ? void 0 : value;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const needsUpdate = previous === void 0 || previous <= 0 || previous > MAX_PACKET_SEND_RATE;
|
|
79
|
+
if (!needsUpdate) return { previous, needsUpdate, content };
|
|
80
|
+
if (keyLine !== -1) {
|
|
81
|
+
lines[keyLine] = lines[keyLine].replace(KEY_RE, (_, prefix, _value, suffix) => `${prefix}${rate}${suffix}`);
|
|
82
|
+
} else if (sectionStart !== -1) {
|
|
83
|
+
lines.splice(sectionStart + 1, 0, `PacketSendRate=${rate}`);
|
|
84
|
+
} else {
|
|
85
|
+
const hasTrailingNewline = lines.length > 0 && lines.at(-1) === "";
|
|
86
|
+
if (hasTrailingNewline) lines.pop();
|
|
87
|
+
if (lines.length > 0 && lines.at(-1).trim() !== "") lines.push("");
|
|
88
|
+
lines.push(`[${STATS_API_SECTION}]`, `PacketSendRate=${rate}`);
|
|
89
|
+
if (hasTrailingNewline) lines.push("");
|
|
90
|
+
}
|
|
91
|
+
return { previous, needsUpdate, content: lines.join(eol) };
|
|
92
|
+
}
|
|
93
|
+
async function exists(file) {
|
|
94
|
+
try {
|
|
95
|
+
await (0, import_promises.access)(file);
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function readJson(file) {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(await (0, import_promises.readFile)(file, "utf8"));
|
|
104
|
+
} catch {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function epicInstallLocation(value) {
|
|
109
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
110
|
+
const { AppName, InstallLocation } = value;
|
|
111
|
+
return AppName === EPIC_APP_NAME && typeof InstallLocation === "string" ? InstallLocation : void 0;
|
|
112
|
+
}
|
|
113
|
+
async function findEpicInstalls(programData) {
|
|
114
|
+
const found = [];
|
|
115
|
+
const launcherInstalled = await readJson(import_node_path.default.join(programData, "Epic", "UnrealEngineLauncher", "LauncherInstalled.dat"));
|
|
116
|
+
const list = launcherInstalled?.InstallationList;
|
|
117
|
+
if (Array.isArray(list)) {
|
|
118
|
+
for (const item of list) {
|
|
119
|
+
const location = epicInstallLocation(item);
|
|
120
|
+
if (location) found.push(location);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const manifestsDir = import_node_path.default.join(programData, "Epic", "EpicGamesLauncher", "Data", "Manifests");
|
|
124
|
+
try {
|
|
125
|
+
for (const file of await (0, import_promises.readdir)(manifestsDir)) {
|
|
126
|
+
if (!file.toLowerCase().endsWith(".item")) continue;
|
|
127
|
+
const location = epicInstallLocation(await readJson(import_node_path.default.join(manifestsDir, file)));
|
|
128
|
+
if (location) found.push(location);
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
async function findSteamInstalls(steamDir) {
|
|
135
|
+
const libraries = [steamDir];
|
|
136
|
+
try {
|
|
137
|
+
const vdf = await (0, import_promises.readFile)(import_node_path.default.join(steamDir, "steamapps", "libraryfolders.vdf"), "utf8");
|
|
138
|
+
for (const match of vdf.matchAll(/"path"\s+"([^"]+)"/g)) libraries.push(match[1].replace(/\\\\/g, "\\"));
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
return libraries.map((library) => import_node_path.default.join(library, "steamapps", "common", "rocketleague"));
|
|
142
|
+
}
|
|
143
|
+
async function findRocketLeagueInstallDirs() {
|
|
144
|
+
if (process.platform !== "win32") return [];
|
|
145
|
+
const env = process.env;
|
|
146
|
+
const programData = env.ProgramData ?? "C:\\ProgramData";
|
|
147
|
+
const programFiles = env.ProgramFiles ?? "C:\\Program Files";
|
|
148
|
+
const programFilesX86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
149
|
+
const candidates = [
|
|
150
|
+
...await findEpicInstalls(programData),
|
|
151
|
+
import_node_path.default.join(programFiles, "Epic Games", "rocketleague"),
|
|
152
|
+
...await findSteamInstalls(import_node_path.default.join(programFilesX86, "Steam"))
|
|
153
|
+
];
|
|
154
|
+
return [...new Set(candidates.map((dir) => import_node_path.default.normalize(dir)))];
|
|
155
|
+
}
|
|
156
|
+
function statsApiIniPaths(installDir) {
|
|
157
|
+
return STATS_API_INI_FILES.map((file) => import_node_path.default.join(installDir, "TAGame", "Config", file));
|
|
158
|
+
}
|
|
159
|
+
async function findStatsApiIni(installDir) {
|
|
160
|
+
const dirs = installDir ? [installDir] : await findRocketLeagueInstallDirs();
|
|
161
|
+
const searched = [];
|
|
162
|
+
for (const dir of dirs) {
|
|
163
|
+
for (const file of statsApiIniPaths(dir)) {
|
|
164
|
+
searched.push(file);
|
|
165
|
+
if (await exists(file)) return { iniPath: file, searched };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { iniPath: void 0, searched };
|
|
169
|
+
}
|
|
170
|
+
function describeProblem(previous) {
|
|
171
|
+
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})`;
|
|
172
|
+
}
|
|
173
|
+
async function ensureStatsApiEnabled(options = {}) {
|
|
174
|
+
const rate = options.packetSendRate ?? DEFAULT_PACKET_SEND_RATE;
|
|
175
|
+
if (!Number.isFinite(rate) || rate <= 0 || rate > MAX_PACKET_SEND_RATE) {
|
|
176
|
+
throw new RangeError(`packetSendRate must be between 1 and ${MAX_PACKET_SEND_RATE}, got ${rate}.`);
|
|
177
|
+
}
|
|
178
|
+
const warn = options.warn === false ? () => {
|
|
179
|
+
} : options.warn ?? ((message) => console.warn(message));
|
|
180
|
+
let iniPath = options.iniPath;
|
|
181
|
+
let searched = iniPath ? [iniPath] : [];
|
|
182
|
+
if (iniPath === void 0) {
|
|
183
|
+
({ iniPath, searched } = await findStatsApiIni(options.installDir));
|
|
184
|
+
}
|
|
185
|
+
let content;
|
|
186
|
+
let encoding;
|
|
187
|
+
try {
|
|
188
|
+
if (!iniPath) throw new Error("not found");
|
|
189
|
+
const buffer = await (0, import_promises.readFile)(iniPath);
|
|
190
|
+
encoding = buffer[0] === 255 && buffer[1] === 254 ? "utf16le" : "utf8";
|
|
191
|
+
content = buffer.toString(encoding);
|
|
192
|
+
} catch {
|
|
193
|
+
return { status: "not-found", searched };
|
|
194
|
+
}
|
|
195
|
+
const edit = editPacketSendRate(content, rate);
|
|
196
|
+
if (!edit.needsUpdate) return { status: "enabled", iniPath, packetSendRate: edit.previous };
|
|
197
|
+
try {
|
|
198
|
+
await (0, import_promises.writeFile)(iniPath, edit.content, encoding);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
warn(
|
|
201
|
+
`[@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.`
|
|
202
|
+
);
|
|
203
|
+
return { status: "write-failed", iniPath, previous: edit.previous, error };
|
|
204
|
+
}
|
|
205
|
+
warn(
|
|
206
|
+
`[@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.`
|
|
207
|
+
);
|
|
208
|
+
return { status: "updated", iniPath, previous: edit.previous, packetSendRate: rate, restartRequired: true };
|
|
209
|
+
}
|
|
210
|
+
var import_promises, import_node_path, STATS_API_SECTION, DEFAULT_PACKET_SEND_RATE, MAX_PACKET_SEND_RATE, STATS_API_INI_FILES, EPIC_APP_NAME, SECTION_RE, KEY_RE;
|
|
211
|
+
var init_config = __esm({
|
|
212
|
+
"src/node/config.ts"() {
|
|
213
|
+
"use strict";
|
|
214
|
+
import_promises = require("fs/promises");
|
|
215
|
+
import_node_path = __toESM(require("path"), 1);
|
|
216
|
+
STATS_API_SECTION = "TAGame.MatchStatsExporter_TA";
|
|
217
|
+
DEFAULT_PACKET_SEND_RATE = 30;
|
|
218
|
+
MAX_PACKET_SEND_RATE = 120;
|
|
219
|
+
STATS_API_INI_FILES = ["TAStatsAPI.ini", "DefaultStatsAPI.ini"];
|
|
220
|
+
EPIC_APP_NAME = "Sugar";
|
|
221
|
+
SECTION_RE = /^\s*\[([^\]]+)\]\s*$/;
|
|
222
|
+
KEY_RE = /^(\s*PacketSendRate\s*=\s*)([^;\r\n]*?)(\s*(?:;.*)?)$/i;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// src/index.ts
|
|
227
|
+
var index_exports = {};
|
|
228
|
+
__export(index_exports, {
|
|
229
|
+
BALL_UNTOUCHED_TEAM_NUM: () => BALL_UNTOUCHED_TEAM_NUM,
|
|
230
|
+
COMMAND_NAMES: () => COMMAND_NAMES,
|
|
231
|
+
CommandValidationError: () => CommandValidationError,
|
|
232
|
+
ConnectionError: () => ConnectionError,
|
|
233
|
+
EVENT_NAMES: () => EVENT_NAMES,
|
|
234
|
+
ListenerError: () => ListenerError,
|
|
235
|
+
NotConnectedError: () => NotConnectedError,
|
|
236
|
+
PERSPECTIVES: () => PERSPECTIVES,
|
|
237
|
+
RocketLeagueStatsClient: () => RocketLeagueStatsClient,
|
|
238
|
+
StatsApiError: () => StatsApiError,
|
|
239
|
+
Team: () => Team,
|
|
240
|
+
allOf: () => allOf,
|
|
241
|
+
arrayOf: () => arrayOf,
|
|
242
|
+
commandDataGuards: () => commandDataGuards,
|
|
243
|
+
default: () => RocketLeagueStatsClient,
|
|
244
|
+
eventDataGuards: () => eventDataGuards,
|
|
245
|
+
isBallHitData: () => isBallHitData,
|
|
246
|
+
isBallLastTouch: () => isBallLastTouch,
|
|
247
|
+
isBoolean: () => isBoolean,
|
|
248
|
+
isBoostPickupData: () => isBoostPickupData,
|
|
249
|
+
isChangePOVData: () => isChangePOVData,
|
|
250
|
+
isClockUpdatedSecondsData: () => isClockUpdatedSecondsData,
|
|
251
|
+
isCommandData: () => isCommandData,
|
|
252
|
+
isCommandName: () => isCommandName,
|
|
253
|
+
isCountdownBeginData: () => isCountdownBeginData,
|
|
254
|
+
isCrossbarHitData: () => isCrossbarHitData,
|
|
255
|
+
isEventData: () => isEventData,
|
|
256
|
+
isEventName: () => isEventName,
|
|
257
|
+
isFocus: () => isFocus,
|
|
258
|
+
isGame: () => isGame,
|
|
259
|
+
isGameBall: () => isGameBall,
|
|
260
|
+
isGameTeam: () => isGameTeam,
|
|
261
|
+
isGoalReplayEndData: () => isGoalReplayEndData,
|
|
262
|
+
isGoalReplayStartData: () => isGoalReplayStartData,
|
|
263
|
+
isGoalReplayWillEndData: () => isGoalReplayWillEndData,
|
|
264
|
+
isGoalScoredData: () => isGoalScoredData,
|
|
265
|
+
isInteger: () => isInteger,
|
|
266
|
+
isLoadReplayData: () => isLoadReplayData,
|
|
267
|
+
isMatchCreatedData: () => isMatchCreatedData,
|
|
268
|
+
isMatchDestroyedData: () => isMatchDestroyedData,
|
|
269
|
+
isMatchEndedData: () => isMatchEndedData,
|
|
270
|
+
isMatchInitializedData: () => isMatchInitializedData,
|
|
271
|
+
isMatchPausedData: () => isMatchPausedData,
|
|
272
|
+
isMatchUnpausedData: () => isMatchUnpausedData,
|
|
273
|
+
isNumber: () => isNumber,
|
|
274
|
+
isPerspective: () => isPerspective,
|
|
275
|
+
isPlayer: () => isPlayer,
|
|
276
|
+
isPlayerJoinedData: () => isPlayerJoinedData,
|
|
277
|
+
isPlayerLeftData: () => isPlayerLeftData,
|
|
278
|
+
isPlayerRef: () => isPlayerRef,
|
|
279
|
+
isPodiumStartData: () => isPodiumStartData,
|
|
280
|
+
isRawEventEnvelope: () => isRawEventEnvelope,
|
|
281
|
+
isRecord: () => isRecord,
|
|
282
|
+
isReplayCreatedData: () => isReplayCreatedData,
|
|
283
|
+
isRoundStartedData: () => isRoundStartedData,
|
|
284
|
+
isSeekReplayData: () => isSeekReplayData,
|
|
285
|
+
isSetGameSpeedData: () => isSetGameSpeedData,
|
|
286
|
+
isSetHUDVisibilityData: () => isSetHUDVisibilityData,
|
|
287
|
+
isSetMatchPausedData: () => isSetMatchPausedData,
|
|
288
|
+
isStatfeedEventData: () => isStatfeedEventData,
|
|
289
|
+
isStatsApiCommand: () => isStatsApiCommand,
|
|
290
|
+
isStatsApiEvent: () => isStatsApiEvent,
|
|
291
|
+
isString: () => isString,
|
|
292
|
+
isUpdateStateData: () => isUpdateStateData,
|
|
293
|
+
isVector: () => isVector,
|
|
294
|
+
oneOf: () => oneOf,
|
|
295
|
+
optional: () => optional,
|
|
296
|
+
parseEventMessage: () => parseEventMessage,
|
|
297
|
+
shape: () => shape
|
|
298
|
+
});
|
|
299
|
+
module.exports = __toCommonJS(index_exports);
|
|
300
|
+
|
|
301
|
+
// src/types/common.ts
|
|
302
|
+
var Team = {
|
|
303
|
+
Blue: 0,
|
|
304
|
+
Orange: 1
|
|
305
|
+
};
|
|
306
|
+
var BALL_UNTOUCHED_TEAM_NUM = 255;
|
|
307
|
+
|
|
308
|
+
// src/types/events.ts
|
|
309
|
+
var EVENT_NAMES = [
|
|
310
|
+
"UpdateState",
|
|
311
|
+
"BallHit",
|
|
312
|
+
"BoostPickup",
|
|
313
|
+
"ClockUpdatedSeconds",
|
|
314
|
+
"CountdownBegin",
|
|
315
|
+
"CrossbarHit",
|
|
316
|
+
"GoalReplayEnd",
|
|
317
|
+
"GoalReplayStart",
|
|
318
|
+
"GoalReplayWillEnd",
|
|
319
|
+
"GoalScored",
|
|
320
|
+
"MatchCreated",
|
|
321
|
+
"MatchDestroyed",
|
|
322
|
+
"MatchEnded",
|
|
323
|
+
"MatchInitialized",
|
|
324
|
+
"MatchPaused",
|
|
325
|
+
"MatchUnpaused",
|
|
326
|
+
"PlayerJoined",
|
|
327
|
+
"PlayerLeft",
|
|
328
|
+
"PodiumStart",
|
|
329
|
+
"ReplayCreated",
|
|
330
|
+
"RoundStarted",
|
|
331
|
+
"StatfeedEvent"
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
// src/types/commands.ts
|
|
335
|
+
var COMMAND_NAMES = [
|
|
336
|
+
"ChangePOV",
|
|
337
|
+
"LoadReplay",
|
|
338
|
+
"SeekReplay",
|
|
339
|
+
"SetGameSpeed",
|
|
340
|
+
"SetHUDVisibility",
|
|
341
|
+
"SetMatchPaused"
|
|
342
|
+
];
|
|
343
|
+
var PERSPECTIVES = [
|
|
344
|
+
/** Freely move around the map. */
|
|
345
|
+
"Fly",
|
|
346
|
+
/** Follow the position of a player or ball. */
|
|
347
|
+
"SoftAttach",
|
|
348
|
+
/** Follow the position and rotation of a player. */
|
|
349
|
+
"HardAttach",
|
|
350
|
+
/** View from a player's perspective. Applied automatically when Focus targets a player and no Perspective is given. */
|
|
351
|
+
"PlayerView",
|
|
352
|
+
/** Automatically spectates. */
|
|
353
|
+
"AutoCam",
|
|
354
|
+
/** Automatically switches between player views. */
|
|
355
|
+
"Camera_Director"
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
// src/guards/primitives.ts
|
|
359
|
+
function isRecord(value) {
|
|
360
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
361
|
+
}
|
|
362
|
+
var isString = (value) => typeof value === "string";
|
|
363
|
+
var isNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
364
|
+
var isInteger = (value) => Number.isInteger(value);
|
|
365
|
+
var isBoolean = (value) => typeof value === "boolean";
|
|
366
|
+
function optional(guard) {
|
|
367
|
+
return (value) => value === void 0 || guard(value);
|
|
368
|
+
}
|
|
369
|
+
function arrayOf(guard) {
|
|
370
|
+
return (value) => Array.isArray(value) && value.every((item) => guard(item));
|
|
371
|
+
}
|
|
372
|
+
function oneOf(values) {
|
|
373
|
+
const set = new Set(values);
|
|
374
|
+
return (value) => set.has(value);
|
|
375
|
+
}
|
|
376
|
+
function shape(spec) {
|
|
377
|
+
const entries = Object.entries(spec);
|
|
378
|
+
return (value) => {
|
|
379
|
+
if (!isRecord(value)) return false;
|
|
380
|
+
for (const [key, guard] of entries) {
|
|
381
|
+
if (!guard(value[key])) return false;
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function allOf(guard, ...checks) {
|
|
387
|
+
return (value) => guard(value) && checks.every((check) => check(value));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/guards/events.ts
|
|
391
|
+
var isVector = shape({ X: isNumber, Y: isNumber, Z: isNumber });
|
|
392
|
+
var isPlayerRef = shape({
|
|
393
|
+
Name: isString,
|
|
394
|
+
Shortcut: isNumber,
|
|
395
|
+
TeamNum: isNumber
|
|
396
|
+
});
|
|
397
|
+
var isBallLastTouch = shape({
|
|
398
|
+
Player: isPlayerRef,
|
|
399
|
+
Speed: isNumber
|
|
400
|
+
});
|
|
401
|
+
var isPlayer = shape({
|
|
402
|
+
Name: isString,
|
|
403
|
+
Shortcut: isNumber,
|
|
404
|
+
TeamNum: isNumber,
|
|
405
|
+
PrimaryId: isString,
|
|
406
|
+
Score: isNumber,
|
|
407
|
+
Goals: isNumber,
|
|
408
|
+
Shots: isNumber,
|
|
409
|
+
Assists: isNumber,
|
|
410
|
+
Saves: isNumber,
|
|
411
|
+
Touches: isNumber,
|
|
412
|
+
CarTouches: isNumber,
|
|
413
|
+
Demos: isNumber,
|
|
414
|
+
Loadout: optional(arrayOf(isString)),
|
|
415
|
+
bHasCar: optional(isBoolean),
|
|
416
|
+
Speed: optional(isNumber),
|
|
417
|
+
Boost: optional(isNumber),
|
|
418
|
+
bBoosting: optional(isBoolean),
|
|
419
|
+
bOnGround: optional(isBoolean),
|
|
420
|
+
bOnWall: optional(isBoolean),
|
|
421
|
+
bPowersliding: optional(isBoolean),
|
|
422
|
+
bDemolished: optional(isBoolean),
|
|
423
|
+
bSupersonic: optional(isBoolean),
|
|
424
|
+
PickupClass: optional(isString),
|
|
425
|
+
Attacker: optional(isPlayerRef)
|
|
426
|
+
});
|
|
427
|
+
var isGameTeam = shape({
|
|
428
|
+
Name: isString,
|
|
429
|
+
TeamNum: isNumber,
|
|
430
|
+
Score: isNumber,
|
|
431
|
+
ColorPrimary: isString,
|
|
432
|
+
ColorSecondary: isString
|
|
433
|
+
});
|
|
434
|
+
var isGameBall = shape({ Speed: isNumber, TeamNum: isNumber });
|
|
435
|
+
var isGame = shape({
|
|
436
|
+
Teams: arrayOf(isGameTeam),
|
|
437
|
+
PlaylistId: optional(isNumber),
|
|
438
|
+
TimeSeconds: isNumber,
|
|
439
|
+
bOvertime: isBoolean,
|
|
440
|
+
Ball: isGameBall,
|
|
441
|
+
bReplay: isBoolean,
|
|
442
|
+
bHasWinner: isBoolean,
|
|
443
|
+
Winner: isString,
|
|
444
|
+
Arena: isString,
|
|
445
|
+
bHasTarget: isBoolean,
|
|
446
|
+
Target: optional(isPlayerRef),
|
|
447
|
+
Frame: optional(isNumber),
|
|
448
|
+
Elapsed: optional(isNumber)
|
|
449
|
+
});
|
|
450
|
+
var matchGuid = optional(isString);
|
|
451
|
+
var isBaseEventData = shape({ MatchGuid: matchGuid });
|
|
452
|
+
var isUpdateStateData = shape({
|
|
453
|
+
MatchGuid: matchGuid,
|
|
454
|
+
Players: arrayOf(isPlayer),
|
|
455
|
+
Game: isGame
|
|
456
|
+
});
|
|
457
|
+
var isBallHitData = shape({
|
|
458
|
+
MatchGuid: matchGuid,
|
|
459
|
+
Players: arrayOf(isPlayerRef),
|
|
460
|
+
Ball: shape({
|
|
461
|
+
PreHitSpeed: isNumber,
|
|
462
|
+
PostHitSpeed: isNumber,
|
|
463
|
+
Location: isVector
|
|
464
|
+
})
|
|
465
|
+
});
|
|
466
|
+
var isBoostPickupData = shape({
|
|
467
|
+
MatchGuid: matchGuid,
|
|
468
|
+
Player: isPlayerRef,
|
|
469
|
+
Location: isVector,
|
|
470
|
+
BoostAmount: isNumber,
|
|
471
|
+
BoostType: oneOf(["BoostType_Pad", "BoostType_Pill"]),
|
|
472
|
+
bReplay: isBoolean
|
|
473
|
+
});
|
|
474
|
+
var isClockUpdatedSecondsData = shape({
|
|
475
|
+
MatchGuid: matchGuid,
|
|
476
|
+
TimeSeconds: isNumber,
|
|
477
|
+
bOvertime: isBoolean
|
|
478
|
+
});
|
|
479
|
+
var isCrossbarHitData = shape({
|
|
480
|
+
MatchGuid: matchGuid,
|
|
481
|
+
BallLocation: isVector,
|
|
482
|
+
BallSpeed: isNumber,
|
|
483
|
+
ImpactForce: isNumber,
|
|
484
|
+
BallLastTouch: isBallLastTouch
|
|
485
|
+
});
|
|
486
|
+
var isGoalScoredData = shape({
|
|
487
|
+
MatchGuid: matchGuid,
|
|
488
|
+
GoalSpeed: isNumber,
|
|
489
|
+
GoalTime: isNumber,
|
|
490
|
+
ImpactLocation: isVector,
|
|
491
|
+
Scorer: isPlayerRef,
|
|
492
|
+
Assister: optional(isPlayerRef),
|
|
493
|
+
BallLastTouch: isBallLastTouch
|
|
494
|
+
});
|
|
495
|
+
var isMatchEndedData = shape({
|
|
496
|
+
MatchGuid: matchGuid,
|
|
497
|
+
WinnerTeamNum: isNumber
|
|
498
|
+
});
|
|
499
|
+
var isPlayerJoinedData = shape({
|
|
500
|
+
MatchGuid: matchGuid,
|
|
501
|
+
PlayerName: isString,
|
|
502
|
+
PrimaryId: isString
|
|
503
|
+
});
|
|
504
|
+
var isPlayerLeftData = shape({
|
|
505
|
+
MatchGuid: matchGuid,
|
|
506
|
+
PlayerName: isString,
|
|
507
|
+
PrimaryId: isString
|
|
508
|
+
});
|
|
509
|
+
var isReplayCreatedData = shape({
|
|
510
|
+
MatchGuid: matchGuid,
|
|
511
|
+
FileName: isString,
|
|
512
|
+
Date: isString
|
|
513
|
+
});
|
|
514
|
+
var isStatfeedEventData = shape({
|
|
515
|
+
MatchGuid: matchGuid,
|
|
516
|
+
EventName: isString,
|
|
517
|
+
Type: isString,
|
|
518
|
+
MainTarget: isPlayerRef,
|
|
519
|
+
SecondaryTarget: optional(isPlayerRef)
|
|
520
|
+
});
|
|
521
|
+
var isCountdownBeginData = isBaseEventData;
|
|
522
|
+
var isGoalReplayEndData = isBaseEventData;
|
|
523
|
+
var isGoalReplayStartData = isBaseEventData;
|
|
524
|
+
var isGoalReplayWillEndData = isBaseEventData;
|
|
525
|
+
var isMatchCreatedData = isBaseEventData;
|
|
526
|
+
var isMatchDestroyedData = isBaseEventData;
|
|
527
|
+
var isMatchInitializedData = isBaseEventData;
|
|
528
|
+
var isMatchPausedData = isBaseEventData;
|
|
529
|
+
var isMatchUnpausedData = isBaseEventData;
|
|
530
|
+
var isPodiumStartData = isBaseEventData;
|
|
531
|
+
var isRoundStartedData = isBaseEventData;
|
|
532
|
+
var eventDataGuards = {
|
|
533
|
+
UpdateState: isUpdateStateData,
|
|
534
|
+
BallHit: isBallHitData,
|
|
535
|
+
BoostPickup: isBoostPickupData,
|
|
536
|
+
ClockUpdatedSeconds: isClockUpdatedSecondsData,
|
|
537
|
+
CountdownBegin: isCountdownBeginData,
|
|
538
|
+
CrossbarHit: isCrossbarHitData,
|
|
539
|
+
GoalReplayEnd: isGoalReplayEndData,
|
|
540
|
+
GoalReplayStart: isGoalReplayStartData,
|
|
541
|
+
GoalReplayWillEnd: isGoalReplayWillEndData,
|
|
542
|
+
GoalScored: isGoalScoredData,
|
|
543
|
+
MatchCreated: isMatchCreatedData,
|
|
544
|
+
MatchDestroyed: isMatchDestroyedData,
|
|
545
|
+
MatchEnded: isMatchEndedData,
|
|
546
|
+
MatchInitialized: isMatchInitializedData,
|
|
547
|
+
MatchPaused: isMatchPausedData,
|
|
548
|
+
MatchUnpaused: isMatchUnpausedData,
|
|
549
|
+
PlayerJoined: isPlayerJoinedData,
|
|
550
|
+
PlayerLeft: isPlayerLeftData,
|
|
551
|
+
PodiumStart: isPodiumStartData,
|
|
552
|
+
ReplayCreated: isReplayCreatedData,
|
|
553
|
+
RoundStarted: isRoundStartedData,
|
|
554
|
+
StatfeedEvent: isStatfeedEventData
|
|
555
|
+
};
|
|
556
|
+
var isEventName = oneOf(EVENT_NAMES);
|
|
557
|
+
function isEventData(name, data) {
|
|
558
|
+
const guard = eventDataGuards[name];
|
|
559
|
+
return guard(data);
|
|
560
|
+
}
|
|
561
|
+
function isRawEventEnvelope(value) {
|
|
562
|
+
return isRecord(value) && isString(value.Event) && (isString(value.Data) || isRecord(value.Data));
|
|
563
|
+
}
|
|
564
|
+
function isStatsApiEvent(value, name) {
|
|
565
|
+
if (!isRecord(value) || !isEventName(value.Event)) return false;
|
|
566
|
+
if (name !== void 0 && value.Event !== name) return false;
|
|
567
|
+
return isEventData(value.Event, value.Data);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/guards/commands.ts
|
|
571
|
+
var isFocus = (value) => value === "Ball" || isString(value) && /^\d+$/.test(value);
|
|
572
|
+
var isPerspective = oneOf(PERSPECTIVES);
|
|
573
|
+
function has(record, key, check) {
|
|
574
|
+
return record[key] !== void 0 && check(record[key]);
|
|
575
|
+
}
|
|
576
|
+
var isChangePOVData = (value) => isRecord(value) && optional(isFocus)(value.Focus) && optional(isPerspective)(value.Perspective) && (value.Focus !== void 0 || value.Perspective !== void 0);
|
|
577
|
+
var isLoadReplayData = (value) => isRecord(value) && optional(isString)(value.FileName) && optional(isString)(value.Path) && (has(value, "FileName", (v) => v !== "") || has(value, "Path", (v) => v !== ""));
|
|
578
|
+
var isSeekReplayData = (value) => isRecord(value) && optional((v) => isInteger(v) && v >= 0)(value.Frame) && optional((v) => isNumber(v) && v >= 0)(value.TimeSeconds) && (value.Frame !== void 0 || value.TimeSeconds !== void 0);
|
|
579
|
+
var isSetGameSpeedData = (value) => isRecord(value) && isNumber(value.Speed) && value.Speed >= 0;
|
|
580
|
+
var isSetHUDVisibilityData = (value) => isRecord(value) && isBoolean(value.bVisible);
|
|
581
|
+
var isSetMatchPausedData = (value) => isRecord(value) && isBoolean(value.bPaused);
|
|
582
|
+
var commandDataGuards = {
|
|
583
|
+
ChangePOV: isChangePOVData,
|
|
584
|
+
LoadReplay: isLoadReplayData,
|
|
585
|
+
SeekReplay: isSeekReplayData,
|
|
586
|
+
SetGameSpeed: isSetGameSpeedData,
|
|
587
|
+
SetHUDVisibility: isSetHUDVisibilityData,
|
|
588
|
+
SetMatchPaused: isSetMatchPausedData
|
|
589
|
+
};
|
|
590
|
+
var isCommandName = oneOf(COMMAND_NAMES);
|
|
591
|
+
function isCommandData(name, data) {
|
|
592
|
+
const guard = commandDataGuards[name];
|
|
593
|
+
return guard(data);
|
|
594
|
+
}
|
|
595
|
+
function isStatsApiCommand(value, name) {
|
|
596
|
+
if (!isRecord(value) || !isCommandName(value.Command)) return false;
|
|
597
|
+
if (name !== void 0 && value.Command !== name) return false;
|
|
598
|
+
return isCommandData(value.Command, value.Data);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/parse.ts
|
|
602
|
+
function parseEventMessage(raw, options = {}) {
|
|
603
|
+
const validate = options.validate ?? true;
|
|
604
|
+
let envelope = raw;
|
|
605
|
+
if (typeof raw === "string") {
|
|
606
|
+
try {
|
|
607
|
+
envelope = JSON.parse(raw);
|
|
608
|
+
} catch {
|
|
609
|
+
return { ok: false, reason: "invalid-json", raw };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (!isRecord(envelope) || typeof envelope.Event !== "string") {
|
|
613
|
+
return { ok: false, reason: "invalid-envelope", raw };
|
|
614
|
+
}
|
|
615
|
+
const event = envelope.Event;
|
|
616
|
+
let data = envelope.Data;
|
|
617
|
+
if (typeof data === "string") {
|
|
618
|
+
try {
|
|
619
|
+
data = JSON.parse(data);
|
|
620
|
+
} catch {
|
|
621
|
+
return { ok: false, reason: "invalid-data-json", raw, event, data };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (!isRecord(data)) {
|
|
625
|
+
return { ok: false, reason: "invalid-data-json", raw, event, data };
|
|
626
|
+
}
|
|
627
|
+
if (!isEventName(event)) {
|
|
628
|
+
return { ok: false, reason: "unknown-event", raw, event, data };
|
|
629
|
+
}
|
|
630
|
+
if (validate && !isEventData(event, data)) {
|
|
631
|
+
return { ok: false, reason: "invalid-data", raw, event, data };
|
|
632
|
+
}
|
|
633
|
+
return { ok: true, event: { Event: event, Data: data } };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/errors.ts
|
|
637
|
+
var StatsApiError = class extends Error {
|
|
638
|
+
name = "StatsApiError";
|
|
639
|
+
};
|
|
640
|
+
var NotConnectedError = class extends StatsApiError {
|
|
641
|
+
name = "NotConnectedError";
|
|
642
|
+
constructor(message = "Cannot send a command: the socket is not connected.") {
|
|
643
|
+
super(message);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
var CommandValidationError = class extends StatsApiError {
|
|
647
|
+
constructor(message, command, data) {
|
|
648
|
+
super(message);
|
|
649
|
+
this.command = command;
|
|
650
|
+
this.data = data;
|
|
651
|
+
}
|
|
652
|
+
command;
|
|
653
|
+
data;
|
|
654
|
+
name = "CommandValidationError";
|
|
655
|
+
};
|
|
656
|
+
var ConnectionError = class extends StatsApiError {
|
|
657
|
+
name = "ConnectionError";
|
|
658
|
+
};
|
|
659
|
+
var ListenerError = class extends StatsApiError {
|
|
660
|
+
constructor(eventName, cause) {
|
|
661
|
+
super(`A listener for "${eventName}" threw an error.`, { cause });
|
|
662
|
+
this.eventName = eventName;
|
|
663
|
+
}
|
|
664
|
+
eventName;
|
|
665
|
+
name = "ListenerError";
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
// src/client.ts
|
|
669
|
+
var WS_OPEN = 1;
|
|
670
|
+
function isNodeRuntime() {
|
|
671
|
+
const proc = globalThis.process;
|
|
672
|
+
return typeof proc?.versions?.node === "string";
|
|
673
|
+
}
|
|
674
|
+
var RocketLeagueStatsClient = class {
|
|
675
|
+
/** The WebSocket URL this client connects to. */
|
|
676
|
+
url;
|
|
677
|
+
/**
|
|
678
|
+
* Result of the `.ini` check (see the `ensureStatsApi` option). `undefined` outside Node.js or when
|
|
679
|
+
* `ensureStatsApi: false`. Never rejects.
|
|
680
|
+
*/
|
|
681
|
+
statsApiConfig;
|
|
682
|
+
#autoConnect;
|
|
683
|
+
#reconnect;
|
|
684
|
+
#validateEvents;
|
|
685
|
+
#WebSocket;
|
|
686
|
+
#socket = null;
|
|
687
|
+
#state = "idle";
|
|
688
|
+
#manualClose = false;
|
|
689
|
+
#reconnectAttempts = 0;
|
|
690
|
+
#reconnectTimer = null;
|
|
691
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
692
|
+
#anyListeners = /* @__PURE__ */ new Set();
|
|
693
|
+
constructor(options = {}) {
|
|
694
|
+
const protocol = options.secure ? "wss" : "ws";
|
|
695
|
+
this.url = options.url ?? `${protocol}://${options.host ?? "127.0.0.1"}:${options.port ?? 49124}`;
|
|
696
|
+
this.#autoConnect = options.autoConnect ?? true;
|
|
697
|
+
this.#validateEvents = options.validateEvents ?? true;
|
|
698
|
+
this.#WebSocket = options.WebSocket ?? globalThis.WebSocket;
|
|
699
|
+
const reconnect = options.reconnect ?? true;
|
|
700
|
+
this.#reconnect = reconnect === false ? null : {
|
|
701
|
+
initialDelay: 1e3,
|
|
702
|
+
maxDelay: 3e4,
|
|
703
|
+
factor: 2,
|
|
704
|
+
maxAttempts: Number.POSITIVE_INFINITY,
|
|
705
|
+
...reconnect === true ? {} : reconnect
|
|
706
|
+
};
|
|
707
|
+
const ensureStatsApi = options.ensureStatsApi ?? true;
|
|
708
|
+
if (ensureStatsApi !== false && isNodeRuntime()) {
|
|
709
|
+
const ensureOptions = ensureStatsApi === true ? {} : ensureStatsApi;
|
|
710
|
+
const rate = ensureOptions.packetSendRate;
|
|
711
|
+
if (rate !== void 0 && !(Number.isFinite(rate) && rate > 0 && rate <= 120)) {
|
|
712
|
+
throw new RangeError(`packetSendRate must be between 1 and 120, got ${rate}.`);
|
|
713
|
+
}
|
|
714
|
+
this.statsApiConfig = Promise.resolve().then(() => (init_config(), config_exports)).then(({ ensureStatsApiEnabled: ensureStatsApiEnabled2 }) => ensureStatsApiEnabled2(ensureOptions));
|
|
715
|
+
} else {
|
|
716
|
+
this.statsApiConfig = void 0;
|
|
717
|
+
}
|
|
718
|
+
if (this.#autoConnect) {
|
|
719
|
+
queueMicrotask(() => {
|
|
720
|
+
if (this.#state === "idle" && !this.#manualClose) this.connect();
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/** Current connection state. */
|
|
725
|
+
get state() {
|
|
726
|
+
return this.#state;
|
|
727
|
+
}
|
|
728
|
+
/** `true` when the socket is open and commands can be sent. */
|
|
729
|
+
get isConnected() {
|
|
730
|
+
return this.#state === "open" && this.#socket?.readyState === WS_OPEN;
|
|
731
|
+
}
|
|
732
|
+
// -----------------------------------------------------------------------
|
|
733
|
+
// Connection
|
|
734
|
+
// -----------------------------------------------------------------------
|
|
735
|
+
/**
|
|
736
|
+
* Opens the connection. Does nothing if already connecting or connected.
|
|
737
|
+
* Called automatically unless `autoConnect: false` was passed.
|
|
738
|
+
*/
|
|
739
|
+
connect() {
|
|
740
|
+
if (this.#state === "connecting" || this.#state === "open") return;
|
|
741
|
+
this.#manualClose = false;
|
|
742
|
+
this.#clearReconnectTimer();
|
|
743
|
+
const WebSocketImpl = this.#WebSocket;
|
|
744
|
+
if (!WebSocketImpl) {
|
|
745
|
+
throw new ConnectionError(
|
|
746
|
+
'No WebSocket implementation found. Use Node.js >= 22, or pass one via the `WebSocket` option (e.g. from the "ws" package).'
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
this.#state = "connecting";
|
|
750
|
+
let socket;
|
|
751
|
+
try {
|
|
752
|
+
socket = new WebSocketImpl(this.url);
|
|
753
|
+
} catch (cause) {
|
|
754
|
+
this.#state = "closed";
|
|
755
|
+
this.#emitError(new ConnectionError(`Failed to create WebSocket for ${this.url}.`, { cause }));
|
|
756
|
+
this.#scheduleReconnect();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
this.#socket = socket;
|
|
760
|
+
socket.addEventListener("open", () => {
|
|
761
|
+
if (this.#socket !== socket) return;
|
|
762
|
+
this.#state = "open";
|
|
763
|
+
this.#reconnectAttempts = 0;
|
|
764
|
+
this.#emit("connect");
|
|
765
|
+
});
|
|
766
|
+
socket.addEventListener("message", (event) => {
|
|
767
|
+
if (this.#socket !== socket) return;
|
|
768
|
+
void this.#handleRawMessage(event.data);
|
|
769
|
+
});
|
|
770
|
+
socket.addEventListener("error", (cause) => {
|
|
771
|
+
if (this.#socket !== socket) return;
|
|
772
|
+
this.#emitError(new ConnectionError(`WebSocket error on ${this.url}.`, { cause }));
|
|
773
|
+
});
|
|
774
|
+
socket.addEventListener("close", (event) => {
|
|
775
|
+
if (this.#socket !== socket) return;
|
|
776
|
+
this.#socket = null;
|
|
777
|
+
this.#state = "closed";
|
|
778
|
+
const willReconnect = this.#scheduleReconnect(true);
|
|
779
|
+
this.#emit("disconnect", { code: event.code, reason: event.reason, willReconnect });
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/** Closes the connection and stops any pending reconnection. */
|
|
783
|
+
disconnect(code = 1e3, reason = "Client disconnect") {
|
|
784
|
+
this.#manualClose = true;
|
|
785
|
+
this.#clearReconnectTimer();
|
|
786
|
+
const socket = this.#socket;
|
|
787
|
+
if (!socket) {
|
|
788
|
+
if (this.#state !== "idle") this.#state = "closed";
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
socket.close(code, reason);
|
|
792
|
+
}
|
|
793
|
+
#scheduleReconnect(deferEmit = false) {
|
|
794
|
+
const options = this.#reconnect;
|
|
795
|
+
if (!options || this.#manualClose || this.#reconnectAttempts >= options.maxAttempts) return false;
|
|
796
|
+
const attempt = ++this.#reconnectAttempts;
|
|
797
|
+
const delay = Math.min(options.initialDelay * options.factor ** (attempt - 1), options.maxDelay);
|
|
798
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
799
|
+
this.#reconnectTimer = null;
|
|
800
|
+
this.connect();
|
|
801
|
+
}, delay);
|
|
802
|
+
const notify = () => this.#emit("reconnecting", { attempt, delay });
|
|
803
|
+
if (deferEmit) queueMicrotask(notify);
|
|
804
|
+
else notify();
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
#clearReconnectTimer() {
|
|
808
|
+
if (this.#reconnectTimer !== null) {
|
|
809
|
+
clearTimeout(this.#reconnectTimer);
|
|
810
|
+
this.#reconnectTimer = null;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// -----------------------------------------------------------------------
|
|
814
|
+
// Incoming events
|
|
815
|
+
// -----------------------------------------------------------------------
|
|
816
|
+
async #handleRawMessage(raw) {
|
|
817
|
+
let text = raw;
|
|
818
|
+
if (raw instanceof ArrayBuffer || ArrayBuffer.isView(raw)) {
|
|
819
|
+
text = new TextDecoder().decode(raw);
|
|
820
|
+
} else if (typeof Blob !== "undefined" && raw instanceof Blob) {
|
|
821
|
+
text = await raw.text();
|
|
822
|
+
}
|
|
823
|
+
this.handleMessage(text);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Decodes, validates and dispatches one raw message. Called for every socket message;
|
|
827
|
+
* exposed so messages from another transport (e.g. the TCP port, a recording) can be fed in.
|
|
828
|
+
*/
|
|
829
|
+
handleMessage(raw) {
|
|
830
|
+
const result = parseEventMessage(raw, { validate: this.#validateEvents });
|
|
831
|
+
if (!result.ok) {
|
|
832
|
+
this.#emit("invalidMessage", result);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const { event } = result;
|
|
836
|
+
this.#dispatch(event.Event, [event.Data, event]);
|
|
837
|
+
for (const listener of [...this.#anyListeners]) {
|
|
838
|
+
this.#invoke("*", listener, [event]);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Listens to a game event (e.g. `'GoalScored'`) or a lifecycle event (e.g. `'connect'`).
|
|
843
|
+
* The listener's arguments are typed from the event name.
|
|
844
|
+
*
|
|
845
|
+
* @returns A function that removes the listener.
|
|
846
|
+
*/
|
|
847
|
+
on(name, listener) {
|
|
848
|
+
let set = this.#listeners.get(name);
|
|
849
|
+
if (!set) {
|
|
850
|
+
set = /* @__PURE__ */ new Set();
|
|
851
|
+
this.#listeners.set(name, set);
|
|
852
|
+
}
|
|
853
|
+
set.add(listener);
|
|
854
|
+
return () => this.off(name, listener);
|
|
855
|
+
}
|
|
856
|
+
/** Like {@link on}, but the listener is removed after its first call. */
|
|
857
|
+
once(name, listener) {
|
|
858
|
+
const wrapper = ((...args) => {
|
|
859
|
+
unsubscribe();
|
|
860
|
+
listener(...args);
|
|
861
|
+
});
|
|
862
|
+
const unsubscribe = this.on(name, wrapper);
|
|
863
|
+
return unsubscribe;
|
|
864
|
+
}
|
|
865
|
+
/** Removes a listener added with {@link on}. */
|
|
866
|
+
off(name, listener) {
|
|
867
|
+
this.#listeners.get(name)?.delete(listener);
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Listens to **every** game event. The argument is a discriminated union: narrow it with `event.Event`.
|
|
871
|
+
*
|
|
872
|
+
* ```ts
|
|
873
|
+
* client.onAny((event) => {
|
|
874
|
+
* if (event.Event === 'StatfeedEvent') console.log(event.Data.EventName);
|
|
875
|
+
* });
|
|
876
|
+
* ```
|
|
877
|
+
*
|
|
878
|
+
* @returns A function that removes the listener.
|
|
879
|
+
*/
|
|
880
|
+
onAny(listener) {
|
|
881
|
+
this.#anyListeners.add(listener);
|
|
882
|
+
return () => this.offAny(listener);
|
|
883
|
+
}
|
|
884
|
+
/** Removes a listener added with {@link onAny}. */
|
|
885
|
+
offAny(listener) {
|
|
886
|
+
this.#anyListeners.delete(listener);
|
|
887
|
+
}
|
|
888
|
+
/** Removes every listener, or every listener of one name. */
|
|
889
|
+
removeAllListeners(name) {
|
|
890
|
+
if (name === void 0) {
|
|
891
|
+
this.#listeners.clear();
|
|
892
|
+
this.#anyListeners.clear();
|
|
893
|
+
} else {
|
|
894
|
+
this.#listeners.delete(name);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Resolves with the payload of the next occurrence of a game event.
|
|
899
|
+
* Rejects after `timeout` ms if provided.
|
|
900
|
+
*/
|
|
901
|
+
waitFor(name, options = {}) {
|
|
902
|
+
return new Promise((resolve, reject) => {
|
|
903
|
+
let timer;
|
|
904
|
+
const unsubscribe = this.once(name, ((data) => {
|
|
905
|
+
if (timer) clearTimeout(timer);
|
|
906
|
+
resolve(data);
|
|
907
|
+
}));
|
|
908
|
+
if (options.timeout !== void 0) {
|
|
909
|
+
timer = setTimeout(() => {
|
|
910
|
+
unsubscribe();
|
|
911
|
+
reject(new Error(`Timed out after ${options.timeout}ms waiting for "${name}".`));
|
|
912
|
+
}, options.timeout);
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
#emit(name, ...args) {
|
|
917
|
+
this.#dispatch(name, args);
|
|
918
|
+
}
|
|
919
|
+
#dispatch(name, args) {
|
|
920
|
+
const set = this.#listeners.get(name);
|
|
921
|
+
if (!set) return;
|
|
922
|
+
for (const listener of [...set]) {
|
|
923
|
+
this.#invoke(name, listener, args);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
#invoke(name, listener, args) {
|
|
927
|
+
try {
|
|
928
|
+
listener(...args);
|
|
929
|
+
} catch (cause) {
|
|
930
|
+
if (name === "error") return;
|
|
931
|
+
this.#emitError(new ListenerError(name, cause));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/** Errors are only reported to `error` listeners; without one they are silently dropped. */
|
|
935
|
+
#emitError(error) {
|
|
936
|
+
this.#emit("error", error);
|
|
937
|
+
}
|
|
938
|
+
send(commandOrEnvelope, maybeData) {
|
|
939
|
+
const [command, data] = typeof commandOrEnvelope === "string" ? [commandOrEnvelope, maybeData] : isRecord(commandOrEnvelope) ? [commandOrEnvelope.Command, commandOrEnvelope.Data] : [commandOrEnvelope, void 0];
|
|
940
|
+
if (!isCommandName(command)) {
|
|
941
|
+
throw new CommandValidationError(`Unknown command "${String(command)}".`, command, data);
|
|
942
|
+
}
|
|
943
|
+
if (!isCommandData(command, data)) {
|
|
944
|
+
throw new CommandValidationError(`Invalid payload for command "${command}".`, command, data);
|
|
945
|
+
}
|
|
946
|
+
const envelope = { Command: command, Data: data };
|
|
947
|
+
if (!isStatsApiCommand(envelope)) {
|
|
948
|
+
throw new CommandValidationError(`Invalid command envelope for "${command}".`, command, data);
|
|
949
|
+
}
|
|
950
|
+
const socket = this.#socket;
|
|
951
|
+
if (!socket || !this.isConnected) throw new NotConnectedError();
|
|
952
|
+
socket.send(JSON.stringify(envelope));
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
956
|
+
0 && (module.exports = {
|
|
957
|
+
BALL_UNTOUCHED_TEAM_NUM,
|
|
958
|
+
COMMAND_NAMES,
|
|
959
|
+
CommandValidationError,
|
|
960
|
+
ConnectionError,
|
|
961
|
+
EVENT_NAMES,
|
|
962
|
+
ListenerError,
|
|
963
|
+
NotConnectedError,
|
|
964
|
+
PERSPECTIVES,
|
|
965
|
+
RocketLeagueStatsClient,
|
|
966
|
+
StatsApiError,
|
|
967
|
+
Team,
|
|
968
|
+
allOf,
|
|
969
|
+
arrayOf,
|
|
970
|
+
commandDataGuards,
|
|
971
|
+
eventDataGuards,
|
|
972
|
+
isBallHitData,
|
|
973
|
+
isBallLastTouch,
|
|
974
|
+
isBoolean,
|
|
975
|
+
isBoostPickupData,
|
|
976
|
+
isChangePOVData,
|
|
977
|
+
isClockUpdatedSecondsData,
|
|
978
|
+
isCommandData,
|
|
979
|
+
isCommandName,
|
|
980
|
+
isCountdownBeginData,
|
|
981
|
+
isCrossbarHitData,
|
|
982
|
+
isEventData,
|
|
983
|
+
isEventName,
|
|
984
|
+
isFocus,
|
|
985
|
+
isGame,
|
|
986
|
+
isGameBall,
|
|
987
|
+
isGameTeam,
|
|
988
|
+
isGoalReplayEndData,
|
|
989
|
+
isGoalReplayStartData,
|
|
990
|
+
isGoalReplayWillEndData,
|
|
991
|
+
isGoalScoredData,
|
|
992
|
+
isInteger,
|
|
993
|
+
isLoadReplayData,
|
|
994
|
+
isMatchCreatedData,
|
|
995
|
+
isMatchDestroyedData,
|
|
996
|
+
isMatchEndedData,
|
|
997
|
+
isMatchInitializedData,
|
|
998
|
+
isMatchPausedData,
|
|
999
|
+
isMatchUnpausedData,
|
|
1000
|
+
isNumber,
|
|
1001
|
+
isPerspective,
|
|
1002
|
+
isPlayer,
|
|
1003
|
+
isPlayerJoinedData,
|
|
1004
|
+
isPlayerLeftData,
|
|
1005
|
+
isPlayerRef,
|
|
1006
|
+
isPodiumStartData,
|
|
1007
|
+
isRawEventEnvelope,
|
|
1008
|
+
isRecord,
|
|
1009
|
+
isReplayCreatedData,
|
|
1010
|
+
isRoundStartedData,
|
|
1011
|
+
isSeekReplayData,
|
|
1012
|
+
isSetGameSpeedData,
|
|
1013
|
+
isSetHUDVisibilityData,
|
|
1014
|
+
isSetMatchPausedData,
|
|
1015
|
+
isStatfeedEventData,
|
|
1016
|
+
isStatsApiCommand,
|
|
1017
|
+
isStatsApiEvent,
|
|
1018
|
+
isString,
|
|
1019
|
+
isUpdateStateData,
|
|
1020
|
+
isVector,
|
|
1021
|
+
oneOf,
|
|
1022
|
+
optional,
|
|
1023
|
+
parseEventMessage,
|
|
1024
|
+
shape
|
|
1025
|
+
});
|
|
1026
|
+
//# sourceMappingURL=index.cjs.map
|