@four-leaf-studios/rl-overlay 1.0.1 → 1.0.3
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/dist/index.cjs.js +161 -97
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +150 -100
- package/dist/index.esm.js.map +1 -1
- package/dist/types/Player.d.ts +1 -0
- package/dist/types/PlayerBoost.d.ts +1 -0
- package/dist/types/Replay.d.ts +1 -1
- package/dist/types/Scoreboard.d.ts +1 -1
- package/dist/types/ScoreboardGameBox.d.ts +1 -1
- package/dist/types/ScoreboardSeriesBox.d.ts +1 -0
- package/dist/types/ScoreboardTeam.d.ts +1 -1
- package/dist/types/StatItem.d.ts +1 -1
- package/dist/types/TargetBoost.d.ts +1 -0
- package/dist/types/TargetPlayer.d.ts +1 -0
- package/dist/types/TargetPlayerLocation.d.ts +1 -0
- package/dist/types/TargetPlayerStats.d.ts +1 -0
- package/dist/types/Team.d.ts +3 -3
- package/dist/types/Teams.d.ts +1 -1
- package/dist/types/Timer.d.ts +1 -1
- package/package.json +2 -2
- package/src/Player.tsx +1 -1
- package/src/PlayerBoost.tsx +1 -1
- package/src/Replay.tsx +1 -1
- package/src/Scoreboard.tsx +1 -1
- package/src/ScoreboardGameBox.tsx +3 -1
- package/src/ScoreboardSeriesBox.tsx +3 -4
- package/src/ScoreboardTeam.tsx +1 -1
- package/src/StatItem.tsx +1 -1
- package/src/TargetBoost.tsx +1 -1
- package/src/TargetPlayer.tsx +1 -2
- package/src/TargetPlayerLocation.tsx +1 -1
- package/src/TargetPlayerStats.tsx +1 -1
- package/src/Team.tsx +3 -3
- package/src/Teams.tsx +1 -1
- package/src/Timer.tsx +1 -1
- package/test-overlay/package.json +27 -27
package/dist/index.esm.js
CHANGED
|
@@ -1,63 +1,161 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import React__default, {
|
|
3
|
-
import { jsx,
|
|
2
|
+
import React__default, { useReducer, useState, useRef, useCallback, useEffect, createContext, useContext, useSyncExternalStore, useMemo, useLayoutEffect, useId, useInsertionEffect, Children, isValidElement, forwardRef, Fragment as Fragment$1, createElement, Component, memo as memo$1 } from 'react';
|
|
3
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
// useRocketLeagueSocket.ts
|
|
6
|
+
const reducer = (state, action) => ({
|
|
7
|
+
...state,
|
|
8
|
+
[action.type]: action.payload,
|
|
9
|
+
});
|
|
10
|
+
const normalizeEvent = (raw) => {
|
|
11
|
+
if (typeof raw === "object" && raw !== null) {
|
|
12
|
+
// { event: "name", data: {...} }
|
|
13
|
+
if (typeof raw.event === "string" && "data" in raw) {
|
|
14
|
+
return [[raw.event, raw.data]];
|
|
15
|
+
}
|
|
16
|
+
// { "evt1": {...}, "evt2": {...} }
|
|
17
|
+
return Object.entries(raw);
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
};
|
|
21
|
+
function useRocketLeagueSocket(url = "ws://localhost:49122", options) {
|
|
22
|
+
const { maxRetries = 5, heartbeatIntervalMs = 30_000 } = options || {};
|
|
23
|
+
// Reducer state holds the map of eventName → payload
|
|
24
|
+
const [events, dispatch] = useReducer(reducer, {});
|
|
25
|
+
const [readyState, setReadyState] = useState(WebSocket.CLOSED);
|
|
26
|
+
const [error, setError] = useState(null);
|
|
27
|
+
// Refs for the socket, retry count, and heartbeat timer
|
|
28
|
+
const socketRef = useRef(null);
|
|
29
|
+
const retryRef = useRef(0);
|
|
30
|
+
const heartbeatRef = useRef(undefined);
|
|
31
|
+
// Expose a send() helper
|
|
32
|
+
const send = useCallback((event, data) => {
|
|
33
|
+
const sock = socketRef.current;
|
|
34
|
+
if (sock?.readyState === WebSocket.OPEN) {
|
|
35
|
+
sock.send(JSON.stringify({ event, data }));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.warn("Cannot send, socket not open:", event);
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
10
41
|
useEffect(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
let isMounted = true;
|
|
43
|
+
const connect = () => {
|
|
44
|
+
const ws = new WebSocket(url);
|
|
45
|
+
socketRef.current = ws;
|
|
46
|
+
setReadyState(ws.readyState);
|
|
47
|
+
ws.onopen = () => {
|
|
48
|
+
if (!isMounted)
|
|
49
|
+
return;
|
|
50
|
+
retryRef.current = 0;
|
|
51
|
+
setReadyState(ws.readyState);
|
|
52
|
+
// Start heartbeat to keep alive
|
|
53
|
+
heartbeatRef.current = window.setInterval(() => {
|
|
54
|
+
ws.send(JSON.stringify({ event: "ping" }));
|
|
55
|
+
}, heartbeatIntervalMs);
|
|
56
|
+
};
|
|
57
|
+
ws.onmessage = (e) => {
|
|
58
|
+
if (!isMounted)
|
|
59
|
+
return;
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = JSON.parse(e.data);
|
|
25
63
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
64
|
+
catch {
|
|
65
|
+
console.error("Invalid JSON from server:", e.data);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// ignore pong replies
|
|
69
|
+
if (raw.event === "pong")
|
|
70
|
+
return;
|
|
71
|
+
// dispatch each normalized event
|
|
72
|
+
for (const [evt, payload] of normalizeEvent(raw)) {
|
|
73
|
+
dispatch({ type: evt, payload });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
ws.onerror = (ev) => {
|
|
77
|
+
if (!isMounted)
|
|
78
|
+
return;
|
|
79
|
+
console.error("WebSocket error", ev);
|
|
80
|
+
setError(ev);
|
|
81
|
+
};
|
|
82
|
+
ws.onclose = () => {
|
|
83
|
+
if (!isMounted)
|
|
84
|
+
return;
|
|
85
|
+
setReadyState(WebSocket.CLOSED);
|
|
86
|
+
if (heartbeatRef.current !== undefined) {
|
|
87
|
+
clearInterval(heartbeatRef.current);
|
|
88
|
+
}
|
|
89
|
+
// attempt reconnect
|
|
90
|
+
if (retryRef.current < maxRetries) {
|
|
91
|
+
const backoff = 2 ** retryRef.current * 1000;
|
|
92
|
+
retryRef.current += 1;
|
|
93
|
+
setTimeout(connect, backoff);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.warn("Reached max WebSocket retries");
|
|
97
|
+
}
|
|
98
|
+
};
|
|
30
99
|
};
|
|
100
|
+
connect();
|
|
31
101
|
return () => {
|
|
32
|
-
|
|
102
|
+
isMounted = false;
|
|
103
|
+
if (heartbeatRef.current !== undefined) {
|
|
104
|
+
clearInterval(heartbeatRef.current);
|
|
105
|
+
}
|
|
106
|
+
socketRef.current?.close();
|
|
33
107
|
};
|
|
34
|
-
}, [url]);
|
|
35
|
-
|
|
108
|
+
}, [url, maxRetries, heartbeatIntervalMs]);
|
|
109
|
+
return { events, readyState, error, send };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const RLContext = createContext(null);
|
|
113
|
+
const RLProvider = ({ url = "ws://localhost:49122", children }) => {
|
|
114
|
+
// 1) pull in every event & helpers from the single socket hook
|
|
115
|
+
const { events: allEvents, send, readyState, error, } = useRocketLeagueSocket(url);
|
|
116
|
+
// 2) Use a loose-typed ref to avoid union-vs-intersection complaints
|
|
117
|
+
const dataRef = useRef({});
|
|
118
|
+
const subsRef = useRef({});
|
|
119
|
+
// 3) Sync incoming allEvents into dataRef and notify subscribers
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
// Force evt to be keyof PayloadStorage for type-narrowing
|
|
122
|
+
const keys = Object.keys(allEvents);
|
|
123
|
+
keys.forEach((evt) => {
|
|
124
|
+
const payload = allEvents[evt];
|
|
125
|
+
// write into our record (string index)
|
|
126
|
+
dataRef.current[evt] = payload;
|
|
127
|
+
// notify anyone subscribed to this key
|
|
128
|
+
(subsRef.current[evt] || []).forEach((cb) => cb());
|
|
129
|
+
});
|
|
130
|
+
}, [allEvents]);
|
|
131
|
+
// 4) Build the same Store API, casting when reading back
|
|
36
132
|
const store = useMemo(() => ({
|
|
37
|
-
getSnapshot: (eventName) =>
|
|
133
|
+
getSnapshot: (eventName) =>
|
|
134
|
+
// cast from any back to the correct PayloadStorage type
|
|
135
|
+
dataRef.current[eventName],
|
|
38
136
|
subscribe: (eventName, callback) => {
|
|
39
|
-
const
|
|
40
|
-
|
|
137
|
+
const arr = (subsRef.current[eventName] ??= []);
|
|
138
|
+
arr.push(callback);
|
|
41
139
|
return () => {
|
|
42
|
-
|
|
140
|
+
subsRef.current[eventName] = arr.filter((c) => c !== callback);
|
|
43
141
|
};
|
|
44
142
|
},
|
|
45
|
-
|
|
143
|
+
send,
|
|
144
|
+
readyState,
|
|
145
|
+
error,
|
|
146
|
+
}), [send, readyState, error]);
|
|
46
147
|
return jsx(RLContext.Provider, { value: store, children: children });
|
|
47
148
|
};
|
|
48
149
|
/**
|
|
49
|
-
* Subscribe to the full
|
|
150
|
+
* Subscribe to the full payload of an event.
|
|
50
151
|
*/
|
|
51
152
|
function useEvent(eventName) {
|
|
52
153
|
const store = useContext(RLContext);
|
|
53
154
|
if (!store)
|
|
54
155
|
throw new Error("useEvent must be inside <RLProvider>");
|
|
55
|
-
// 2) (optional) explicitly tell TS what snapshot type is
|
|
56
156
|
return useSyncExternalStore((cb) => store.subscribe(eventName, cb), () => store.getSnapshot(eventName));
|
|
57
157
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Tiny deep-equal for JavaScript values
|
|
60
|
-
*/
|
|
158
|
+
/** Tiny deep-equal for selector comparisons */
|
|
61
159
|
function deepEqual(a, b) {
|
|
62
160
|
if (Object.is(a, b))
|
|
63
161
|
return true;
|
|
@@ -67,34 +165,21 @@ function deepEqual(a, b) {
|
|
|
67
165
|
b === null) {
|
|
68
166
|
return false;
|
|
69
167
|
}
|
|
70
|
-
const aKeys = Object.keys(a);
|
|
71
|
-
const bKeys = Object.keys(b);
|
|
168
|
+
const aKeys = Object.keys(a), bKeys = Object.keys(b);
|
|
72
169
|
if (aKeys.length !== bKeys.length)
|
|
73
170
|
return false;
|
|
74
|
-
|
|
75
|
-
if (!deepEqual(a[key], b[key]))
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
return true;
|
|
171
|
+
return aKeys.every((key) => deepEqual(a[key], b[key]));
|
|
79
172
|
}
|
|
80
173
|
/**
|
|
81
|
-
* Subscribe to a selected slice of the event payload.
|
|
82
|
-
* Uses a built-in deep-equality check by default (no lodash).
|
|
174
|
+
* Subscribe to a selected slice of the event payload. Re-renders only on actual changes.
|
|
83
175
|
*/
|
|
84
|
-
function useEventSelector(
|
|
85
|
-
/** Event key to subscribe to */
|
|
86
|
-
eventName,
|
|
87
|
-
/** Selector that picks a slice of the payload */
|
|
88
|
-
selector,
|
|
89
|
-
/** Equality function to compare selector results; defaults to deepEqual */
|
|
90
|
-
isEqual = deepEqual) {
|
|
176
|
+
function useEventSelector(eventName, selector, isEqual = deepEqual) {
|
|
91
177
|
const store = useContext(RLContext);
|
|
92
178
|
const lastRef = useRef({});
|
|
93
179
|
const subscribe = useCallback((cb) => store.subscribe(eventName, cb), [eventName, store]);
|
|
94
180
|
const getSnapshot = useCallback(() => {
|
|
95
181
|
const raw = store.getSnapshot(eventName);
|
|
96
182
|
const next = selector(raw);
|
|
97
|
-
// deep-equal by default
|
|
98
183
|
if (lastRef.current.val !== undefined &&
|
|
99
184
|
isEqual(next, lastRef.current.val)) {
|
|
100
185
|
return lastRef.current.val;
|
|
@@ -105,51 +190,16 @@ isEqual = deepEqual) {
|
|
|
105
190
|
return useSyncExternalStore(subscribe, getSnapshot);
|
|
106
191
|
}
|
|
107
192
|
|
|
108
|
-
const
|
|
109
|
-
...state,
|
|
110
|
-
[action.type]: action.payload,
|
|
111
|
-
});
|
|
112
|
-
const normalizeEvent = (raw) => {
|
|
113
|
-
if (typeof raw === "object") {
|
|
114
|
-
// Format 1: { event: "game:update_state", data: { ... } }
|
|
115
|
-
if (typeof raw.event === "string" && "data" in raw) {
|
|
116
|
-
return [[raw.event, raw.data]];
|
|
117
|
-
}
|
|
118
|
-
// Format 2: { "game:update_state": { ... }, "game:goal_scored": { ... } }
|
|
119
|
-
return Object.entries(raw);
|
|
120
|
-
}
|
|
121
|
-
return [];
|
|
122
|
-
};
|
|
123
|
-
const useRocketLeagueSocket = (url = "ws://localhost:49122") => {
|
|
124
|
-
const [state, dispatch] = useReducer(reducer, {});
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
const socket = new WebSocket(url);
|
|
127
|
-
socket.onmessage = (event) => {
|
|
128
|
-
try {
|
|
129
|
-
const rawData = JSON.parse(event.data);
|
|
130
|
-
const events = normalizeEvent(rawData);
|
|
131
|
-
events.forEach(([eventName, payload]) => {
|
|
132
|
-
dispatch({ type: eventName, payload });
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
console.error("WebSocket error: Invalid JSON", err);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
return () => socket.close();
|
|
140
|
-
}, [url]);
|
|
141
|
-
return state;
|
|
142
|
-
};
|
|
143
|
-
|
|
193
|
+
const STATE_LABELS = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
|
|
144
194
|
const WebsocketData = () => {
|
|
145
|
-
const
|
|
146
|
-
return (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
195
|
+
const { events, readyState, error } = useRocketLeagueSocket();
|
|
196
|
+
return (jsxs("div", { style: { fontFamily: "monospace", padding: "1rem" }, children: [jsx("h1", { children: "Rocket League Live Events" }), jsxs("p", { children: [jsx("strong", { children: "Connection:" }), " ", STATE_LABELS[readyState] ?? readyState] }), error && (jsxs("p", { style: { color: "red" }, children: [jsx("strong", { children: "Error:" }), " ", String(error)] })), Object.entries(events).map(([event, payload]) => (jsxs("div", { style: {
|
|
197
|
+
border: "1px solid #ccc",
|
|
198
|
+
marginBottom: "1rem",
|
|
199
|
+
padding: "0.5rem",
|
|
200
|
+
borderRadius: "4px",
|
|
201
|
+
backgroundColor: "#f9f9f9",
|
|
202
|
+
}, children: [jsx("h2", { children: event }), jsx("pre", { style: { whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: JSON.stringify(payload, null, 2) })] }, event))), Object.keys(events).length === 0 && jsx("p", { children: "No events received yet." })] }));
|
|
153
203
|
};
|
|
154
204
|
|
|
155
205
|
var BroadcastContext = createContext(undefined);
|
|
@@ -10687,5 +10737,5 @@ Overlay.TargetPlayer = TargetPlayer$1;
|
|
|
10687
10737
|
Overlay.TargetBoost = TargetBoost$1;
|
|
10688
10738
|
Overlay.Replay = Replay;
|
|
10689
10739
|
|
|
10690
|
-
export { Overlay, RLContext, RLProvider, WebsocketData, useEvent, useEventSelector, useOverlayStyles, useRocketLeagueSocket };
|
|
10740
|
+
export { Overlay, Player, PlayerBoost, RLContext, RLProvider, Replay, Scoreboard, ScoreboardGameBox, ScoreboardSeriesBoxComponent, ScoreboardTeam, StatItem, TargetBoost, TargetPlayer, TargetPlayerLocation, TargetPlayerStats, Team, Timer, WebsocketData, useEvent, useEventSelector, useOverlayStyles, useRocketLeagueSocket };
|
|
10691
10741
|
//# sourceMappingURL=index.esm.js.map
|