@rydr/game-sdk 9.3.0 → 9.4.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/dist/net/SharedClock.d.ts +152 -0
- package/dist/net/SharedClock.d.ts.map +1 -0
- package/dist/net/SharedClock.js +195 -0
- package/dist/net/SharedClock.js.map +1 -0
- package/dist/net/index.d.ts +50 -0
- package/dist/net/index.d.ts.map +1 -0
- package/dist/net/index.js +50 -0
- package/dist/net/index.js.map +1 -0
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/dist/training/README.md +291 -0
- package/dist/training/compile.d.ts.map +1 -1
- package/dist/training/compile.js +13 -2
- package/dist/training/compile.js.map +1 -1
- package/package.json +10 -4
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SHARED CLOCK — agreement on what time it is, for everyone in a room (PLAT-1456).
|
|
3
|
+
*
|
|
4
|
+
* Lifted from Racing's `LiveClock` (RACI-131/213), which proved the design over a season of real
|
|
5
|
+
* races, and made transport-neutral so the shell can run one too. It is the whole of what a
|
|
6
|
+
* synchronised experience needs from the network: derive a timetable from a timestamp, and the only
|
|
7
|
+
* thing left to agree is the timestamp.
|
|
8
|
+
*
|
|
9
|
+
* offset = serverAt + rtt / 2 - now (rtt/2 ≈ the return leg)
|
|
10
|
+
*
|
|
11
|
+
* Several samples are taken and the one measured through the LOWEST latency wins — the standard
|
|
12
|
+
* NTP-ish trick, because a sample delayed by a slow return leg is the one whose `rtt/2` estimate is
|
|
13
|
+
* least trustworthy.
|
|
14
|
+
*
|
|
15
|
+
* **With no transport at all the offset stays 0 and {@link SharedClock.serverNow} is local time.**
|
|
16
|
+
* That is deliberate and load-bearing: a rider alone in a room, a standalone dev session and the
|
|
17
|
+
* SDK's loopback room must all run the same code path as a full field, because most of the time
|
|
18
|
+
* they will be the case.
|
|
19
|
+
*
|
|
20
|
+
* ## It re-syncs, and it reports what it measured
|
|
21
|
+
*
|
|
22
|
+
* Measurement continues for the life of the clock, in ROUNDS of {@link DEFAULT_SAMPLES} spaced
|
|
23
|
+
* {@link DEFAULT_RESYNC_MS} apart, and each round adopts its OWN best sample. Racing originally took
|
|
24
|
+
* three samples at boot and kept the lowest forever, which quietly made two things impossible: a
|
|
25
|
+
* connection that changed route mid-session could never correct its offset (the old "best" was
|
|
26
|
+
* unbeatable), and nothing downstream could know how bad the link was. Both matter once latency is a
|
|
27
|
+
* gameplay number rather than only a countdown's accuracy.
|
|
28
|
+
*
|
|
29
|
+
* ## A stamp is only yours if the server says it is
|
|
30
|
+
*
|
|
31
|
+
* Orchestration events are **broadcast to the whole room**, so every member sees every other
|
|
32
|
+
* member's probe reply. Racing's two wiring sites both fed those replies straight into
|
|
33
|
+
* `applyStamp` on the strength of the token alone (`RoomSession.ts`, `seriesRoom.ts`) — and tokens
|
|
34
|
+
* were per-client sequential (`s0`, `s1`, …), so two riders syncing at once would each apply the
|
|
35
|
+
* other's server stamp against their own send time and corrupt their offset. Nothing surfaced it:
|
|
36
|
+
* the error is a plausible-looking number.
|
|
37
|
+
*
|
|
38
|
+
* Fixed here twice over, because this is the kind of bug that comes back:
|
|
39
|
+
*
|
|
40
|
+
* 1. {@link SharedClock.applyStamp} takes `from` and **ignores any stamp not sent by `selfId`**.
|
|
41
|
+
* `RoomEvent.from` is stamped by the server and cannot be forged by a peer.
|
|
42
|
+
* 2. Tokens carry a per-instance random prefix, so even an unguarded caller cannot collide.
|
|
43
|
+
*
|
|
44
|
+
* Skew shows up as a mistimed transition, never as corrupted measurement: whatever the clock is
|
|
45
|
+
* used to schedule is timed locally. Worst case a badly-skewed member acts a fraction late.
|
|
46
|
+
*/
|
|
47
|
+
/** How many round trips one sync ROUND measures before settling on an offset. */
|
|
48
|
+
export declare const DEFAULT_SAMPLES = 3;
|
|
49
|
+
/** Gap between round trips within a round (ms) — spread so one congested moment can't skew all. */
|
|
50
|
+
export declare const DEFAULT_SAMPLE_INTERVAL_MS = 400;
|
|
51
|
+
/**
|
|
52
|
+
* Gap between sync ROUNDS (ms). Measurement continues for the whole session: a member's route can
|
|
53
|
+
* change mid-session (wifi to cellular, a colo failover), and both the offset and the reported
|
|
54
|
+
* {@link SharedClock.rttMs} have to follow it.
|
|
55
|
+
*
|
|
56
|
+
* Far cheaper than it looks — three ~60-byte events a round — and it bounds how long a stale
|
|
57
|
+
* estimate can survive.
|
|
58
|
+
*/
|
|
59
|
+
export declare const DEFAULT_RESYNC_MS = 30000;
|
|
60
|
+
/**
|
|
61
|
+
* The timer functions the clock schedules against. Injectable so a test can run rounds
|
|
62
|
+
* deterministically instead of waiting 30 real seconds; defaults to the host environment's.
|
|
63
|
+
*/
|
|
64
|
+
export interface ClockTimers {
|
|
65
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
66
|
+
clearTimeout(handle: unknown): void;
|
|
67
|
+
setInterval(fn: () => void, ms: number): unknown;
|
|
68
|
+
clearInterval(handle: unknown): void;
|
|
69
|
+
}
|
|
70
|
+
export interface SharedClockOptions {
|
|
71
|
+
/**
|
|
72
|
+
* This member's own id. **Stamps from anyone else are ignored** — see the note on broadcast
|
|
73
|
+
* replies in this file's header. Pass the same `playerId` the room knows you by.
|
|
74
|
+
*/
|
|
75
|
+
selfId: string;
|
|
76
|
+
/** Samples per round. Defaults to {@link DEFAULT_SAMPLES}. */
|
|
77
|
+
samples?: number;
|
|
78
|
+
/** Gap between samples within a round. Defaults to {@link DEFAULT_SAMPLE_INTERVAL_MS}. */
|
|
79
|
+
sampleIntervalMs?: number;
|
|
80
|
+
/** Gap between rounds. Defaults to {@link DEFAULT_RESYNC_MS}. */
|
|
81
|
+
resyncIntervalMs?: number;
|
|
82
|
+
/** Local time source. Injectable for tests; defaults to `Date.now`. */
|
|
83
|
+
now?: () => number;
|
|
84
|
+
/** Timer source. Injectable for tests; defaults to the global timers. */
|
|
85
|
+
timers?: ClockTimers;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Tracks the offset between this machine's clock and the room server's, and reports
|
|
89
|
+
* {@link serverNow}.
|
|
90
|
+
*
|
|
91
|
+
* **ONE PER ROOM SESSION, not one per activity.** The offset is measured against the room server, so
|
|
92
|
+
* it is a property of the room; a clock rebuilt for each activity reports raw local time for its
|
|
93
|
+
* first round trip after every transition, which is exactly when it is most relied upon.
|
|
94
|
+
*/
|
|
95
|
+
export declare class SharedClock {
|
|
96
|
+
private readonly selfId;
|
|
97
|
+
private readonly samples;
|
|
98
|
+
private readonly sampleIntervalMs;
|
|
99
|
+
private readonly resyncIntervalMs;
|
|
100
|
+
private readonly now;
|
|
101
|
+
private readonly timers;
|
|
102
|
+
/** ms to add to local time to land on server time. 0 until a sample lands (and when alone). */
|
|
103
|
+
private offset;
|
|
104
|
+
/**
|
|
105
|
+
* Best latency measured in the CURRENT round — reset when a round opens, so each round adopts its
|
|
106
|
+
* own fastest sample rather than being locked out by an old one.
|
|
107
|
+
*/
|
|
108
|
+
private roundBestRtt;
|
|
109
|
+
/** Best latency of the last COMPLETED round, which is what {@link rttMs} reports. */
|
|
110
|
+
private lastRoundRtt;
|
|
111
|
+
/** Local send times of round trips still in flight, keyed by the token echoed back. */
|
|
112
|
+
private readonly inFlight;
|
|
113
|
+
private pending;
|
|
114
|
+
private roundTimer;
|
|
115
|
+
private nextToken;
|
|
116
|
+
/** Per-instance token prefix — belt to the `from` guard's braces (see the header). */
|
|
117
|
+
private readonly tokenPrefix;
|
|
118
|
+
private disposed;
|
|
119
|
+
constructor(options: SharedClockOptions);
|
|
120
|
+
/**
|
|
121
|
+
* Start measuring against the server. `probe` is handed a token to round-trip; whatever transport
|
|
122
|
+
* carries it must feed the reply back through {@link applyStamp} with the same token and the
|
|
123
|
+
* server's `from`.
|
|
124
|
+
*
|
|
125
|
+
* Safe to never call — the clock then reports local time, which is the correct answer for a member
|
|
126
|
+
* who is alone.
|
|
127
|
+
*/
|
|
128
|
+
startSync(probe: (token: string) => void): void;
|
|
129
|
+
/**
|
|
130
|
+
* Feed back a server-stamped reply: `serverAt` is the server's own clock reading for the event it
|
|
131
|
+
* broadcast, and `from` is the member the server says scheduled it.
|
|
132
|
+
*
|
|
133
|
+
* Ignores anything that is not this member's own outstanding probe — another member's sync, a
|
|
134
|
+
* replay, or a stale token after {@link dispose}.
|
|
135
|
+
*/
|
|
136
|
+
applyStamp(token: string, serverAt: number, from: string): void;
|
|
137
|
+
/** Epoch ms on the shared clock. Local time until a sample lands. */
|
|
138
|
+
serverNow(): number;
|
|
139
|
+
/** True once a round trip has landed — i.e. the clock is server-aligned, not merely local. */
|
|
140
|
+
get synced(): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Round-trip time (ms) from the best sample of the latest completed round, or null before the
|
|
143
|
+
* first one lands (and forever for a member with no transport, who has nothing to measure).
|
|
144
|
+
*
|
|
145
|
+
* Half of it is the one-way estimate — that is the assumption the offset is already built on, so
|
|
146
|
+
* nothing downstream may assume a better one.
|
|
147
|
+
*/
|
|
148
|
+
get rttMs(): number | null;
|
|
149
|
+
/** Stop pending round trips and the repeating round. */
|
|
150
|
+
dispose(): void;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=SharedClock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SharedClock.d.ts","sourceRoot":"","sources":["../../src/net/SharedClock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,iFAAiF;AACjF,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,mGAAmG;AACnG,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAExC;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,WAAW,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACjD,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,yEAAyE;IACzE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AASD;;;;;;;GAOG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC,+FAA+F;IAC/F,OAAO,CAAC,MAAM,CAAK;IACnB;;;OAGG;IACH,OAAO,CAAC,YAAY,CAAY;IAChC,qFAAqF;IACrF,OAAO,CAAC,YAAY,CAAY;IAChC,uFAAuF;IACvF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,SAAS,CAAK;IACtB,sFAAsF;IACtF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0C;IACtE,OAAO,CAAC,QAAQ,CAAS;gBAEb,OAAO,EAAE,kBAAkB;IASvC;;;;;;;OAOG;IACH,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAyB/C;;;;;;OAMG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAgB/D,qEAAqE;IACrE,SAAS,IAAI,MAAM;IAInB,8FAA8F;IAC9F,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;;;;;OAMG;IACH,IAAI,KAAK,IAAI,MAAM,GAAG,IAAI,CAEzB;IAED,wDAAwD;IACxD,OAAO,IAAI,IAAI;CAQhB"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SHARED CLOCK — agreement on what time it is, for everyone in a room (PLAT-1456).
|
|
3
|
+
*
|
|
4
|
+
* Lifted from Racing's `LiveClock` (RACI-131/213), which proved the design over a season of real
|
|
5
|
+
* races, and made transport-neutral so the shell can run one too. It is the whole of what a
|
|
6
|
+
* synchronised experience needs from the network: derive a timetable from a timestamp, and the only
|
|
7
|
+
* thing left to agree is the timestamp.
|
|
8
|
+
*
|
|
9
|
+
* offset = serverAt + rtt / 2 - now (rtt/2 ≈ the return leg)
|
|
10
|
+
*
|
|
11
|
+
* Several samples are taken and the one measured through the LOWEST latency wins — the standard
|
|
12
|
+
* NTP-ish trick, because a sample delayed by a slow return leg is the one whose `rtt/2` estimate is
|
|
13
|
+
* least trustworthy.
|
|
14
|
+
*
|
|
15
|
+
* **With no transport at all the offset stays 0 and {@link SharedClock.serverNow} is local time.**
|
|
16
|
+
* That is deliberate and load-bearing: a rider alone in a room, a standalone dev session and the
|
|
17
|
+
* SDK's loopback room must all run the same code path as a full field, because most of the time
|
|
18
|
+
* they will be the case.
|
|
19
|
+
*
|
|
20
|
+
* ## It re-syncs, and it reports what it measured
|
|
21
|
+
*
|
|
22
|
+
* Measurement continues for the life of the clock, in ROUNDS of {@link DEFAULT_SAMPLES} spaced
|
|
23
|
+
* {@link DEFAULT_RESYNC_MS} apart, and each round adopts its OWN best sample. Racing originally took
|
|
24
|
+
* three samples at boot and kept the lowest forever, which quietly made two things impossible: a
|
|
25
|
+
* connection that changed route mid-session could never correct its offset (the old "best" was
|
|
26
|
+
* unbeatable), and nothing downstream could know how bad the link was. Both matter once latency is a
|
|
27
|
+
* gameplay number rather than only a countdown's accuracy.
|
|
28
|
+
*
|
|
29
|
+
* ## A stamp is only yours if the server says it is
|
|
30
|
+
*
|
|
31
|
+
* Orchestration events are **broadcast to the whole room**, so every member sees every other
|
|
32
|
+
* member's probe reply. Racing's two wiring sites both fed those replies straight into
|
|
33
|
+
* `applyStamp` on the strength of the token alone (`RoomSession.ts`, `seriesRoom.ts`) — and tokens
|
|
34
|
+
* were per-client sequential (`s0`, `s1`, …), so two riders syncing at once would each apply the
|
|
35
|
+
* other's server stamp against their own send time and corrupt their offset. Nothing surfaced it:
|
|
36
|
+
* the error is a plausible-looking number.
|
|
37
|
+
*
|
|
38
|
+
* Fixed here twice over, because this is the kind of bug that comes back:
|
|
39
|
+
*
|
|
40
|
+
* 1. {@link SharedClock.applyStamp} takes `from` and **ignores any stamp not sent by `selfId`**.
|
|
41
|
+
* `RoomEvent.from` is stamped by the server and cannot be forged by a peer.
|
|
42
|
+
* 2. Tokens carry a per-instance random prefix, so even an unguarded caller cannot collide.
|
|
43
|
+
*
|
|
44
|
+
* Skew shows up as a mistimed transition, never as corrupted measurement: whatever the clock is
|
|
45
|
+
* used to schedule is timed locally. Worst case a badly-skewed member acts a fraction late.
|
|
46
|
+
*/
|
|
47
|
+
/** How many round trips one sync ROUND measures before settling on an offset. */
|
|
48
|
+
export const DEFAULT_SAMPLES = 3;
|
|
49
|
+
/** Gap between round trips within a round (ms) — spread so one congested moment can't skew all. */
|
|
50
|
+
export const DEFAULT_SAMPLE_INTERVAL_MS = 400;
|
|
51
|
+
/**
|
|
52
|
+
* Gap between sync ROUNDS (ms). Measurement continues for the whole session: a member's route can
|
|
53
|
+
* change mid-session (wifi to cellular, a colo failover), and both the offset and the reported
|
|
54
|
+
* {@link SharedClock.rttMs} have to follow it.
|
|
55
|
+
*
|
|
56
|
+
* Far cheaper than it looks — three ~60-byte events a round — and it bounds how long a stale
|
|
57
|
+
* estimate can survive.
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_RESYNC_MS = 30_000;
|
|
60
|
+
const globalTimers = {
|
|
61
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
62
|
+
clearTimeout: (h) => clearTimeout(h),
|
|
63
|
+
setInterval: (fn, ms) => setInterval(fn, ms),
|
|
64
|
+
clearInterval: (h) => clearInterval(h),
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Tracks the offset between this machine's clock and the room server's, and reports
|
|
68
|
+
* {@link serverNow}.
|
|
69
|
+
*
|
|
70
|
+
* **ONE PER ROOM SESSION, not one per activity.** The offset is measured against the room server, so
|
|
71
|
+
* it is a property of the room; a clock rebuilt for each activity reports raw local time for its
|
|
72
|
+
* first round trip after every transition, which is exactly when it is most relied upon.
|
|
73
|
+
*/
|
|
74
|
+
export class SharedClock {
|
|
75
|
+
selfId;
|
|
76
|
+
samples;
|
|
77
|
+
sampleIntervalMs;
|
|
78
|
+
resyncIntervalMs;
|
|
79
|
+
now;
|
|
80
|
+
timers;
|
|
81
|
+
/** ms to add to local time to land on server time. 0 until a sample lands (and when alone). */
|
|
82
|
+
offset = 0;
|
|
83
|
+
/**
|
|
84
|
+
* Best latency measured in the CURRENT round — reset when a round opens, so each round adopts its
|
|
85
|
+
* own fastest sample rather than being locked out by an old one.
|
|
86
|
+
*/
|
|
87
|
+
roundBestRtt = Infinity;
|
|
88
|
+
/** Best latency of the last COMPLETED round, which is what {@link rttMs} reports. */
|
|
89
|
+
lastRoundRtt = Infinity;
|
|
90
|
+
/** Local send times of round trips still in flight, keyed by the token echoed back. */
|
|
91
|
+
inFlight = new Map();
|
|
92
|
+
pending = [];
|
|
93
|
+
roundTimer = null;
|
|
94
|
+
nextToken = 0;
|
|
95
|
+
/** Per-instance token prefix — belt to the `from` guard's braces (see the header). */
|
|
96
|
+
tokenPrefix = Math.random().toString(36).slice(2, 8);
|
|
97
|
+
disposed = false;
|
|
98
|
+
constructor(options) {
|
|
99
|
+
this.selfId = options.selfId;
|
|
100
|
+
this.samples = options.samples ?? DEFAULT_SAMPLES;
|
|
101
|
+
this.sampleIntervalMs = options.sampleIntervalMs ?? DEFAULT_SAMPLE_INTERVAL_MS;
|
|
102
|
+
this.resyncIntervalMs = options.resyncIntervalMs ?? DEFAULT_RESYNC_MS;
|
|
103
|
+
this.now = options.now ?? Date.now;
|
|
104
|
+
this.timers = options.timers ?? globalTimers;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Start measuring against the server. `probe` is handed a token to round-trip; whatever transport
|
|
108
|
+
* carries it must feed the reply back through {@link applyStamp} with the same token and the
|
|
109
|
+
* server's `from`.
|
|
110
|
+
*
|
|
111
|
+
* Safe to never call — the clock then reports local time, which is the correct answer for a member
|
|
112
|
+
* who is alone.
|
|
113
|
+
*/
|
|
114
|
+
startSync(probe) {
|
|
115
|
+
const round = () => {
|
|
116
|
+
if (this.disposed)
|
|
117
|
+
return;
|
|
118
|
+
// Close the round that just ran before opening the next, so `rttMs` always reports a COMPLETE
|
|
119
|
+
// round rather than the first sample of the one in progress.
|
|
120
|
+
if (this.roundBestRtt < Infinity)
|
|
121
|
+
this.lastRoundRtt = this.roundBestRtt;
|
|
122
|
+
this.roundBestRtt = Infinity;
|
|
123
|
+
// Last round's timers have all fired (a round is ~1.2s, the gap 30s), so the array is dead
|
|
124
|
+
// weight — dropped here rather than grown for the length of the session.
|
|
125
|
+
this.pending = [];
|
|
126
|
+
for (let i = 0; i < this.samples; i++) {
|
|
127
|
+
this.pending.push(this.timers.setTimeout(() => {
|
|
128
|
+
if (this.disposed)
|
|
129
|
+
return;
|
|
130
|
+
const token = `${this.tokenPrefix}-${this.nextToken++}`;
|
|
131
|
+
this.inFlight.set(token, this.now());
|
|
132
|
+
probe(token);
|
|
133
|
+
}, i * this.sampleIntervalMs));
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
round();
|
|
137
|
+
this.roundTimer = this.timers.setInterval(round, this.resyncIntervalMs);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Feed back a server-stamped reply: `serverAt` is the server's own clock reading for the event it
|
|
141
|
+
* broadcast, and `from` is the member the server says scheduled it.
|
|
142
|
+
*
|
|
143
|
+
* Ignores anything that is not this member's own outstanding probe — another member's sync, a
|
|
144
|
+
* replay, or a stale token after {@link dispose}.
|
|
145
|
+
*/
|
|
146
|
+
applyStamp(token, serverAt, from) {
|
|
147
|
+
if (from !== this.selfId)
|
|
148
|
+
return; // somebody else's round trip, broadcast to us
|
|
149
|
+
const sentAt = this.inFlight.get(token);
|
|
150
|
+
if (sentAt === undefined)
|
|
151
|
+
return;
|
|
152
|
+
this.inFlight.delete(token);
|
|
153
|
+
const now = this.now();
|
|
154
|
+
const rtt = now - sentAt;
|
|
155
|
+
// A slower sample tells us less than the one we already have THIS ROUND. Across rounds it wins
|
|
156
|
+
// outright, which is what lets a changed route correct an offset that used to be unbeatable.
|
|
157
|
+
if (rtt >= this.roundBestRtt)
|
|
158
|
+
return;
|
|
159
|
+
this.roundBestRtt = rtt;
|
|
160
|
+
// Seed the report from the first round too, so a readout says something before a round closes.
|
|
161
|
+
if (this.lastRoundRtt === Infinity)
|
|
162
|
+
this.lastRoundRtt = rtt;
|
|
163
|
+
this.offset = serverAt + rtt / 2 - now;
|
|
164
|
+
}
|
|
165
|
+
/** Epoch ms on the shared clock. Local time until a sample lands. */
|
|
166
|
+
serverNow() {
|
|
167
|
+
return this.now() + this.offset;
|
|
168
|
+
}
|
|
169
|
+
/** True once a round trip has landed — i.e. the clock is server-aligned, not merely local. */
|
|
170
|
+
get synced() {
|
|
171
|
+
return this.lastRoundRtt < Infinity;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Round-trip time (ms) from the best sample of the latest completed round, or null before the
|
|
175
|
+
* first one lands (and forever for a member with no transport, who has nothing to measure).
|
|
176
|
+
*
|
|
177
|
+
* Half of it is the one-way estimate — that is the assumption the offset is already built on, so
|
|
178
|
+
* nothing downstream may assume a better one.
|
|
179
|
+
*/
|
|
180
|
+
get rttMs() {
|
|
181
|
+
return this.lastRoundRtt < Infinity ? this.lastRoundRtt : null;
|
|
182
|
+
}
|
|
183
|
+
/** Stop pending round trips and the repeating round. */
|
|
184
|
+
dispose() {
|
|
185
|
+
this.disposed = true;
|
|
186
|
+
for (const t of this.pending)
|
|
187
|
+
this.timers.clearTimeout(t);
|
|
188
|
+
this.pending = [];
|
|
189
|
+
if (this.roundTimer !== null)
|
|
190
|
+
this.timers.clearInterval(this.roundTimer);
|
|
191
|
+
this.roundTimer = null;
|
|
192
|
+
this.inFlight.clear();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=SharedClock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SharedClock.js","sourceRoot":"","sources":["../../src/net/SharedClock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rydr/game-sdk/net` — the shared pieces of talking to a room, owned once (PLAT-1456).
|
|
3
|
+
*
|
|
4
|
+
* Racing and Party each built these independently, and the shell now needs them too, which is the
|
|
5
|
+
* third time and the reason they moved here. What lives in this module is only what is genuinely
|
|
6
|
+
* generic: **agreement on the time**. Anything that knows what a lap, a round or a workout block is
|
|
7
|
+
* belongs to its own consumer.
|
|
8
|
+
*
|
|
9
|
+
* The binder below matters as much as the clock does. Both of Racing's wiring sites got the same
|
|
10
|
+
* detail wrong in the same way — applying any member's broadcast probe reply as if it were their own
|
|
11
|
+
* — so the wiring is published here rather than left as an example to copy.
|
|
12
|
+
*/
|
|
13
|
+
export { SharedClock, DEFAULT_SAMPLES, DEFAULT_SAMPLE_INTERVAL_MS, DEFAULT_RESYNC_MS, type ClockTimers, type SharedClockOptions, } from "./SharedClock.js";
|
|
14
|
+
import { SharedClock, type SharedClockOptions } from "./SharedClock.js";
|
|
15
|
+
/**
|
|
16
|
+
* The event name the clock's probes ride on. Exported so a consumer that fans room events out to its
|
|
17
|
+
* own handlers can recognise and swallow them — a sync probe is plumbing, not an orchestration event
|
|
18
|
+
* anybody else should see.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CLOCK_EVENT = "rydr/clock";
|
|
21
|
+
/**
|
|
22
|
+
* The slice of a room the clock needs: schedule a server-stamped event, and hear them back.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately structural rather than `RoomHandle`, so the SHELL's own room object satisfies it too.
|
|
25
|
+
* The shell holds a socket and a guest holds a relay handle; neither difference reaches this far.
|
|
26
|
+
*/
|
|
27
|
+
export interface ClockRoom {
|
|
28
|
+
scheduleEvent(name: string, payload?: unknown, at?: number): void;
|
|
29
|
+
on(event: "event", cb: (e: {
|
|
30
|
+
name: string;
|
|
31
|
+
payload: unknown;
|
|
32
|
+
at: number;
|
|
33
|
+
from: string;
|
|
34
|
+
}) => void): () => void;
|
|
35
|
+
}
|
|
36
|
+
/** A bound clock, plus the unsubscribe that detaches it from the room. */
|
|
37
|
+
export interface BoundClock {
|
|
38
|
+
clock: SharedClock;
|
|
39
|
+
/** Detach from the room and stop measuring. Idempotent. */
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Bind a {@link SharedClock} to a room and start it measuring.
|
|
44
|
+
*
|
|
45
|
+
* One per room session — see the note on {@link SharedClock}. `selfId` must be the id the room knows
|
|
46
|
+
* this member by, because it is what distinguishes this member's probe replies from the broadcast
|
|
47
|
+
* copies of everybody else's.
|
|
48
|
+
*/
|
|
49
|
+
export declare function bindRoomClock(room: ClockRoom, options: SharedClockOptions): BoundClock;
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/net/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,WAAW,EACX,eAAe,EACf,0BAA0B,EAC1B,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,WAAW,eAAe,CAAC;AAExC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClE,EAAE,CACA,KAAK,EAAE,OAAO,EACd,EAAE,EAAE,CAAC,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAC5E,MAAM,IAAI,CAAC;CACf;AAED,0EAA0E;AAC1E,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,2DAA2D;IAC3D,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,kBAAkB,GAC1B,UAAU,CAkBZ"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rydr/game-sdk/net` — the shared pieces of talking to a room, owned once (PLAT-1456).
|
|
3
|
+
*
|
|
4
|
+
* Racing and Party each built these independently, and the shell now needs them too, which is the
|
|
5
|
+
* third time and the reason they moved here. What lives in this module is only what is genuinely
|
|
6
|
+
* generic: **agreement on the time**. Anything that knows what a lap, a round or a workout block is
|
|
7
|
+
* belongs to its own consumer.
|
|
8
|
+
*
|
|
9
|
+
* The binder below matters as much as the clock does. Both of Racing's wiring sites got the same
|
|
10
|
+
* detail wrong in the same way — applying any member's broadcast probe reply as if it were their own
|
|
11
|
+
* — so the wiring is published here rather than left as an example to copy.
|
|
12
|
+
*/
|
|
13
|
+
export { SharedClock, DEFAULT_SAMPLES, DEFAULT_SAMPLE_INTERVAL_MS, DEFAULT_RESYNC_MS, } from "./SharedClock.js";
|
|
14
|
+
import { SharedClock } from "./SharedClock.js";
|
|
15
|
+
/**
|
|
16
|
+
* The event name the clock's probes ride on. Exported so a consumer that fans room events out to its
|
|
17
|
+
* own handlers can recognise and swallow them — a sync probe is plumbing, not an orchestration event
|
|
18
|
+
* anybody else should see.
|
|
19
|
+
*/
|
|
20
|
+
export const CLOCK_EVENT = "rydr/clock";
|
|
21
|
+
/**
|
|
22
|
+
* Bind a {@link SharedClock} to a room and start it measuring.
|
|
23
|
+
*
|
|
24
|
+
* One per room session — see the note on {@link SharedClock}. `selfId` must be the id the room knows
|
|
25
|
+
* this member by, because it is what distinguishes this member's probe replies from the broadcast
|
|
26
|
+
* copies of everybody else's.
|
|
27
|
+
*/
|
|
28
|
+
export function bindRoomClock(room, options) {
|
|
29
|
+
const clock = new SharedClock(options);
|
|
30
|
+
const off = room.on("event", (e) => {
|
|
31
|
+
if (e.name !== CLOCK_EVENT)
|
|
32
|
+
return;
|
|
33
|
+
if (typeof e.payload !== "string")
|
|
34
|
+
return;
|
|
35
|
+
clock.applyStamp(e.payload, e.at, e.from);
|
|
36
|
+
});
|
|
37
|
+
clock.startSync((token) => room.scheduleEvent(CLOCK_EVENT, token));
|
|
38
|
+
let disposed = false;
|
|
39
|
+
return {
|
|
40
|
+
clock,
|
|
41
|
+
dispose() {
|
|
42
|
+
if (disposed)
|
|
43
|
+
return;
|
|
44
|
+
disposed = true;
|
|
45
|
+
off();
|
|
46
|
+
clock.dispose();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/net/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,WAAW,EACX,eAAe,EACf,0BAA0B,EAC1B,iBAAiB,GAGlB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,WAAW,EAA2B,MAAM,kBAAkB,CAAC;AAExE;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC;AAuBxC;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAe,EACf,OAA2B;IAE3B,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;QACjC,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW;YAAE,OAAO;QACnC,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO;QAC1C,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;IACnE,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,OAAO;QACL,KAAK;QACL,OAAO;YACL,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,GAAG,EAAE,CAAC;YACN,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
export declare const RYDR_PROTOCOL_VERSION: 37;
|
|
10
10
|
/** Semver of this SDK build. Sent in the handshake for telemetry/debugging.
|
|
11
11
|
* (Bumped to 2.0.0 by `npm version major` on release — see CHANGELOG [Unreleased].) */
|
|
12
|
-
export declare const RYDR_SDK_VERSION = "9.
|
|
12
|
+
export declare const RYDR_SDK_VERSION = "9.4.0";
|
|
13
13
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/protocol/version.js
CHANGED
|
@@ -334,5 +334,5 @@
|
|
|
334
334
|
export const RYDR_PROTOCOL_VERSION = 37;
|
|
335
335
|
/** Semver of this SDK build. Sent in the handshake for telemetry/debugging.
|
|
336
336
|
* (Bumped to 2.0.0 by `npm version major` on release — see CHANGELOG [Unreleased].) */
|
|
337
|
-
export const RYDR_SDK_VERSION = "9.
|
|
337
|
+
export const RYDR_SDK_VERSION = "9.4.0";
|
|
338
338
|
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Multiplayer workouts — what your game has to do
|
|
2
|
+
|
|
3
|
+
> **For game authors.** You are adapting a game so it can be played inside a workout that a room of
|
|
4
|
+
> riders is doing together. This is the guest-facing half; the shell-side design lives in the
|
|
5
|
+
> platform repo and you never need to read it.
|
|
6
|
+
|
|
7
|
+
## The shape of it
|
|
8
|
+
|
|
9
|
+
A room of riders rides **one workout, together, on one clock**. Alongside it the room holds an
|
|
10
|
+
ordered list of games, and the **host cycles through them** from the platform's OPTIONS menu. Your
|
|
11
|
+
game is one entry in that rotation: when the host reaches it, you are the thing on screen, until they
|
|
12
|
+
move on.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
workout 20' warm-up │ 3 × 13 × 30/15 … 39 efforts … │ 20' cool-down
|
|
16
|
+
│ │
|
|
17
|
+
games ── GAME A ───┴──── GAME B ──────┴─── GAME C ───┴──── (cycles) ──▶
|
|
18
|
+
▲ ▲ ▲
|
|
19
|
+
host presses NEXT GAME whenever they like
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**The two are independent.** A game change can land anywhere — between sets, mid-effort, halfway
|
|
23
|
+
through the warm-up. The workout does not pause for it and the rotation does not wait for a boundary.
|
|
24
|
+
|
|
25
|
+
The platform owns the workout, the clock, the trainer and the room. **You own what is on screen.**
|
|
26
|
+
|
|
27
|
+
## Three rules, and everything else follows from them
|
|
28
|
+
|
|
29
|
+
### 1. You do not own the clock
|
|
30
|
+
|
|
31
|
+
Position comes from `session.training`, which is authoritative and identical for every rider in the
|
|
32
|
+
room. Never run your own timer, never derive elapsed time from when your iframe
|
|
33
|
+
mounted, and never assume your `requestAnimationFrame` count means anything.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
session.training.subscribe((snapshot) => {
|
|
37
|
+
const w = snapshot.workout;
|
|
38
|
+
if (!w) return; // no workout attached — no group session is running
|
|
39
|
+
const { position, targetPower, segment } = w.progress;
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A game that keeps its own clock desynchronises from the rest of the room within a minute, and the
|
|
44
|
+
riders see it long before you do.
|
|
45
|
+
|
|
46
|
+
> **Two different things are called "phase", and mixing them up is the first bug everyone writes.**
|
|
47
|
+
> `progress.phase` is the ENGINE's state — `"active"`, paused, complete — i.e. whether the workout is
|
|
48
|
+
> running at all. The work/recovery rhythm is `progress.segment.type`. You almost always want the
|
|
49
|
+
> second one.
|
|
50
|
+
|
|
51
|
+
### 2. You do not own power
|
|
52
|
+
|
|
53
|
+
The workout prescribes watts and the platform holds the trainer there in ERG. **Do not call
|
|
54
|
+
`setDemand`** — a workout target outranks it, so the call does nothing except make your code lie
|
|
55
|
+
about what is happening.
|
|
56
|
+
|
|
57
|
+
This is the part that decides whether your game is a good fit at all. If your game's controller *is*
|
|
58
|
+
the pedals — go faster by pushing harder — then the workout has already taken your controller
|
|
59
|
+
away: everyone is held at the same target, so everyone performs identically. **A game suited to a
|
|
60
|
+
workout takes its input from the gamepad**: aim, steer, time a press, hit a note. Power is
|
|
61
|
+
spoken for.
|
|
62
|
+
|
|
63
|
+
You still *read* power (`session.hardware`) — actual measured watts, which is what the rider really
|
|
64
|
+
did and how well they held the target. Read it to score adherence, not to move the player.
|
|
65
|
+
|
|
66
|
+
### 3. You do not decide when your turn ends
|
|
67
|
+
|
|
68
|
+
The **host** does, from the OPTIONS menu, and you get no warning. If your song finishes or your wave
|
|
69
|
+
is cleared while you are still on screen, **show something until they move on** — results, a summary,
|
|
70
|
+
an idle loop. Reporting a stage as finished advances nothing in a group workout.
|
|
71
|
+
|
|
72
|
+
Equally, you can be taken off mid-anything. Stop cleanly, and score only what the rider actually did
|
|
73
|
+
while you were up — never invent a result for the part that did not happen.
|
|
74
|
+
|
|
75
|
+
## The new thing: position is not monotonic
|
|
76
|
+
|
|
77
|
+
**The host has admin powers over the running workout** — skip a segment, restart a segment, pause,
|
|
78
|
+
resume — from the platform's OPTIONS menu. These are shell-owned controls; your game neither draws
|
|
79
|
+
them nor receives them as commands.
|
|
80
|
+
|
|
81
|
+
What reaches you is simply the next `session.training` snapshot, **with `position` somewhere you did
|
|
82
|
+
not expect**:
|
|
83
|
+
|
|
84
|
+
| Host does | What you see |
|
|
85
|
+
|---|---|
|
|
86
|
+
| Skip segment | `position` jumps **forward**, `segment` changes |
|
|
87
|
+
| Restart segment | `position` jumps **backward** to the segment start |
|
|
88
|
+
| Pause / resume | `position` stops advancing, then resumes |
|
|
89
|
+
|
|
90
|
+
So the one real requirement this contract adds: **handle a discontinuous position, in both
|
|
91
|
+
directions, at any moment.** Concretely —
|
|
92
|
+
|
|
93
|
+
- Derive your state *from* `position` each tick rather than accumulating it. Anything you integrate
|
|
94
|
+
(a score, a distance, a spawn schedule) needs an explicit answer for "the clock went backwards".
|
|
95
|
+
- Do not replay cues you already fired just because time moved back over them, and do not skip the
|
|
96
|
+
ones you jumped past as though the rider earned them.
|
|
97
|
+
- Re-read `segment` on every snapshot. A skip changes which effort is running, and the workout may
|
|
98
|
+
simply be over.
|
|
99
|
+
|
|
100
|
+
A game that assumes time only moves forward looks fine for a whole session and then breaks the first
|
|
101
|
+
time a host restarts a set — which is exactly when a room full of people is watching.
|
|
102
|
+
|
|
103
|
+
## The v1 session, hard-coded
|
|
104
|
+
|
|
105
|
+
v1 ships **one** workout. It is not picked from a library and not editable; it is this, so that
|
|
106
|
+
everybody adapting a game is aiming at the same session.
|
|
107
|
+
|
|
108
|
+
**3 × 13 × 30/15, with a 20-minute warm-up and a 20-minute cool-down.** A Rønnestad session: three
|
|
109
|
+
sets of thirteen 30-second efforts, 15 seconds easy between them, 3 minutes between sets.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import type { Workout } from "@rydr/game-sdk";
|
|
113
|
+
|
|
114
|
+
const SESSION: Workout = {
|
|
115
|
+
id: "group-ronnestad-3x13",
|
|
116
|
+
name: "3 × 13 × 30/15",
|
|
117
|
+
description: "Rønnestad intervals with a 20' warm-up and 20' cool-down.",
|
|
118
|
+
category: "aerobicPower",
|
|
119
|
+
createdAt: 0,
|
|
120
|
+
updatedAt: 0,
|
|
121
|
+
segments: [
|
|
122
|
+
{ id: "warmup", type: "ramp", duration: 1200, startPower: 50, endPower: 75 },
|
|
123
|
+
{
|
|
124
|
+
id: "main", type: "intervals",
|
|
125
|
+
sets: 3, reps: 13,
|
|
126
|
+
workDuration: 30, workPower: 120,
|
|
127
|
+
recoveryDuration: 15, recoveryPower: 50,
|
|
128
|
+
restBetweenSets: 180, restPower: 45,
|
|
129
|
+
},
|
|
130
|
+
{ id: "cooldown", type: "ramp", duration: 1200, startPower: 60, endPower: 40 },
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Which compiles to **75:15** total — 20:00 + (3 × 13 × 45s = 29:15) + (2 × 3:00 rest) + 20:00. Rest
|
|
136
|
+
goes *between* sets and never after the last one, so three sets means two rests.
|
|
137
|
+
|
|
138
|
+
**Games are assigned to none of this.** The segments above shape the *effort* — what the trainer
|
|
139
|
+
holds and what `segment.type` reports — and nothing else. Which game is on screen while a given
|
|
140
|
+
segment plays is the host's live choice, not a property of the workout.
|
|
141
|
+
|
|
142
|
+
### Games are a rotation, not a block assignment
|
|
143
|
+
|
|
144
|
+
**Games are not tied to the workout's structure at all.** The room holds an ordered list of games and
|
|
145
|
+
a current index; the **host advances it** from the platform's OPTIONS menu, whenever they like, and
|
|
146
|
+
the list cycles. Three games and a 75-minute session means the host decides when each gets its turn.
|
|
147
|
+
|
|
148
|
+
So your game does **not** own a block, a set or a segment. It owns *the screen, until the host moves
|
|
149
|
+
on.* It might be up for one effort or for forty minutes.
|
|
150
|
+
|
|
151
|
+
Two things follow, and the second is the one that will bite you:
|
|
152
|
+
|
|
153
|
+
**You can be mounted at ANY workout position.** Not at a block boundary — at second 2347, fourteen
|
|
154
|
+
seconds into a 30-second effort, with 16 seconds left on it. Render correctly from a cold start at an
|
|
155
|
+
arbitrary position: read `position`, `segment`, `segmentRemaining` and `targetPower` on your very
|
|
156
|
+
first snapshot and draw the truth immediately. Never animate in from zero, never assume you saw the
|
|
157
|
+
effort begin, never wait for the next boundary to start being useful.
|
|
158
|
+
|
|
159
|
+
**You can be unmounted at any position too**, mid-effort included. Stop cleanly and score only what
|
|
160
|
+
the rider actually did while you were on screen.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const p = snapshot.workout?.progress;
|
|
164
|
+
// What the rider is doing right now. NOT `p.phase` — that is the engine's running/paused state.
|
|
165
|
+
const effort = p?.segment?.type; // "work" | "recovery" | "rest" | "steady"
|
|
166
|
+
const set = p?.set; // 1-based, 1..3 — and 0 outside an interval set
|
|
167
|
+
const rep = p?.rep; // 1-based, 1..13 — and 0 outside an interval set
|
|
168
|
+
const watts = p?.targetPower; // what THIS rider should be pushing, watts
|
|
169
|
+
const left = p?.segmentRemaining; // seconds left in this effort — your countdown
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
If your game wants a beat, `phase` is it. Thirty seconds hard, fifteen easy, thirteen times — that
|
|
173
|
+
rhythm is the session, and a game that ignores it is just wallpaper over a workout.
|
|
174
|
+
|
|
175
|
+
## Per-rider watts, shared everything else
|
|
176
|
+
|
|
177
|
+
Every rider rides the same effort at the same %FTP, which is a **different number of watts** for each
|
|
178
|
+
of them. That is the point, and it is already handled: the compiler scales against each rider's own
|
|
179
|
+
FTP, so `targetPower` in your snapshot is *this* rider's target, in watts.
|
|
180
|
+
|
|
181
|
+
**So never send watts to other riders as though they were comparable**, and never rank a room by raw
|
|
182
|
+
power. What is shared is the workout and the position. What is personal is the wattage and how well
|
|
183
|
+
they held it — and the comparable number is how far ABOVE their own target each one is, in watts.
|
|
184
|
+
See "Compare the watts a rider is OVER their own target" below.
|
|
185
|
+
|
|
186
|
+
## Who else is riding — and why you must not ask which are real
|
|
187
|
+
|
|
188
|
+
> **Not shipped yet (PLAT-1667).** The shape below is agreed; the host does not populate it in the
|
|
189
|
+
> current release. Design your rendering against it, but check the field exists before you read it.
|
|
190
|
+
|
|
191
|
+
A workout is ridden by a **room** — and a room is never empty, because a rider alone is given a
|
|
192
|
+
**bot pacer** doing the same session. Solo and group are the same path with a different number of
|
|
193
|
+
participants, so your game must never have a "nobody else is here" branch.
|
|
194
|
+
|
|
195
|
+
**`participants` are the REAL riders. The pacer is yours to make** (Ben's call, 2026-09-22.) An
|
|
196
|
+
earlier draft had the host derive bots and deliver them in this list so that no game had to. That
|
|
197
|
+
seam was wrong: drawing a bot is not a detail of a bot, it *is* the game — a pacer is a rigged
|
|
198
|
+
cyclist on a road in one game and an opponent in a duel in the next, and once the game-specific part
|
|
199
|
+
is removed there is no rendering left for anyone else to own.
|
|
200
|
+
|
|
201
|
+
So the platform gives you the workout, the shared position and the humans. **Filling an empty seat is
|
|
202
|
+
your job**, and it is not optional: a room of one must still be a race. Two rules survive from when
|
|
203
|
+
this was the host's problem, because they were never about rendering —
|
|
204
|
+
|
|
205
|
+
- **A bot is derived, never a member.** Compute it from the frozen workout, the shared position and a
|
|
206
|
+
seed; do not synthesise one into the roster, give it a `playerId`, or put it on the wire.
|
|
207
|
+
- **A bot is not a person in any record.** Never write one into a saved result as an opponent, and
|
|
208
|
+
never let one enter a leaderboard.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const riders = snapshot.workout?.participants ?? [];
|
|
212
|
+
|
|
213
|
+
for (const r of riders) {
|
|
214
|
+
// Compare the watts they are OVER their own target — not compliance, not raw watts.
|
|
215
|
+
draw(r.name, r.power - r.targetPower); // r.isBot changes NOTHING about how you draw them
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Draw your bots exactly as you draw humans.** Whatever you use to mark one — the `isBot` flag on a
|
|
220
|
+
participant, or simply knowing you made it yourself — must change nothing about the geometry. A game
|
|
221
|
+
that renders its pacer differently shows a solo rider something that looks broken, which is precisely
|
|
222
|
+
the case bots exist to fix. Marking a bot is for wording ("you beat Astrid" vs "you beat the pacer")
|
|
223
|
+
and for keeping it out of leaderboards, never for how it is drawn.
|
|
224
|
+
|
|
225
|
+
### There is one position, and it belongs to everybody
|
|
226
|
+
|
|
227
|
+
Participants do **not** each carry a position. The workout's `progress.position` is shared — that is
|
|
228
|
+
what makes it a group workout. Everyone is in the same effort, at the same second, always.
|
|
229
|
+
|
|
230
|
+
So the thing that differs between riders, and the only thing worth drawing a gap from, is **how well
|
|
231
|
+
each is holding the target**.
|
|
232
|
+
|
|
233
|
+
### Compare the watts a rider is OVER their own target
|
|
234
|
+
|
|
235
|
+
**Ben's call, 2026-09-22 (PLAT-1668).** This reverses what this section used to say. If you find a
|
|
236
|
+
comment, a plan or a game arguing for compliance, this paragraph wins.
|
|
237
|
+
|
|
238
|
+
There are three different numbers here and only two of them used to be written down:
|
|
239
|
+
|
|
240
|
+
| Field | Comparable across riders? |
|
|
241
|
+
|---|---|
|
|
242
|
+
| `power - targetPower` | **Yes — this is your comparison.** The watts a rider is above or below their own prescription. |
|
|
243
|
+
| `compliance` (`power / targetPower`) | Yes, but it is not what we use. See below. |
|
|
244
|
+
| `power` | **No.** Their raw watts, meaningful only against their own FTP. |
|
|
245
|
+
| `targetPower` | No. Scaled to their FTP. |
|
|
246
|
+
|
|
247
|
+
Raw `power` is still wrong and for the reason it always was: rank by it and you have built a
|
|
248
|
+
heaviest-rider-wins leaderboard, which is the thing structured training exists to avoid. **That
|
|
249
|
+
warning does not apply to the delta.** `power - targetPower` is already personal to each rider —
|
|
250
|
+
each is measured against their own scaled target — it simply normalises by *subtracting* the target
|
|
251
|
+
rather than by *dividing* by it.
|
|
252
|
+
|
|
253
|
+
So a rider at 200 W FTP pushing +30 W and a rider at 400 W FTP pushing +30 W are a **dead heat**.
|
|
254
|
+
Compliance would have called that 115 % against 107.5 % and given it to the first rider.
|
|
255
|
+
|
|
256
|
+
**Why additive and not multiplicative.** Two reasons, and the second is the one that decided it:
|
|
257
|
+
|
|
258
|
+
- **Watts are the thing being contested.** In a game where riders push against each other, 30 W of
|
|
259
|
+
extra work is 30 W whoever produced it. A percentage is a ratio of prescriptions, not an amount of
|
|
260
|
+
work.
|
|
261
|
+
- **A rider can learn one number.** "I need +15 W" is true in every set, at every target, all
|
|
262
|
+
session — so it can be learned once and acted on at threshold, where nobody is doing arithmetic.
|
|
263
|
+
Under compliance the watts you must find change with every block, and the rider is left computing
|
|
264
|
+
their own target all session.
|
|
265
|
+
|
|
266
|
+
**The tradeoff this accepts, stated plainly:** +30 W is a harder ask of a 150 W rider than of a
|
|
267
|
+
400 W rider, so the smaller rider is doing more relative work for the same result. That is known and
|
|
268
|
+
accepted — it is the same tradeoff a flat safe-zone band already makes, and it is the price of a
|
|
269
|
+
number a rider can hold in their head.
|
|
270
|
+
|
|
271
|
+
### A bot is derived, not connected
|
|
272
|
+
|
|
273
|
+
Bots are computed from the workout, the shared clock and a seed — identical on every client, with no
|
|
274
|
+
network traffic. Practically, for you: a bot never disconnects, never lags, never needs
|
|
275
|
+
interpolating, and its numbers are as trustworthy as your own. You do not need the netcode you would
|
|
276
|
+
need for a human rival.
|
|
277
|
+
|
|
278
|
+
## Checklist
|
|
279
|
+
|
|
280
|
+
- [ ] Reads position from `session.training`, never from a local timer
|
|
281
|
+
- [ ] Survives `position` jumping forward *and* backward, at any moment
|
|
282
|
+
- [ ] Re-reads `segment` every snapshot; notices when the effort changed
|
|
283
|
+
- [ ] Never calls `setDemand`
|
|
284
|
+
- [ ] Renders correctly when mounted at an arbitrary position, mid-effort included
|
|
285
|
+
- [ ] Fills its turn for as long as the host leaves it up, and stops cleanly when taken off
|
|
286
|
+
- [ ] Scores from actual measured watts, not from the prescribed target
|
|
287
|
+
- [ ] Takes its gameplay input from the gamepad, not the pedals
|
|
288
|
+
- [ ] Draws bots and humans identically — however you mark one, it must not reach the geometry
|
|
289
|
+
- [ ] **Makes its own pacer**, so a room of one is still a race — `participants` carries only humans
|
|
290
|
+
- [ ] Keeps its bots off the wire and out of results and leaderboards
|
|
291
|
+
- [ ] Ranks and compares riders by `power - targetPower`, never by raw watts (PLAT-1668)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/training/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAGrE;;;;GAIG;AACH,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtE;AA6CD,iFAAiF;AACjF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,eAAe;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,cAAc,GACvB,WAAW,EAAE,CAoHf;
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/training/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAGrE;;;;GAIG;AACH,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtE;AA6CD,iFAAiF;AACjF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,eAAe;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,cAAc,GACvB,WAAW,EAAE,CAoHf;AA+DD,4CAA4C;AAC5C,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,OAAO,EAAE,MAAM,GACd,WAAW,GAAG,IAAI,CAWpB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAIvF;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgBvE;AAMD,MAAM,WAAW,cAAc;IAC7B,eAAe;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,gBAAgB,EAAE,MAAM,CAAC;IACzB,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,eAAe,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAClC,GAAG,EAAE,MAAM,GACV,cAAc,CAwEhB"}
|
package/dist/training/compile.js
CHANGED
|
@@ -199,10 +199,20 @@ export function compileWorkout(segments, ftp, options) {
|
|
|
199
199
|
function numberReps(flat) {
|
|
200
200
|
let set = 1;
|
|
201
201
|
let rep = 0;
|
|
202
|
+
/**
|
|
203
|
+
* Sets already closed by a rest that no interval set emitted.
|
|
204
|
+
*
|
|
205
|
+
* An interval set numbers from 1 because it only knows about itself. Author a session as several
|
|
206
|
+
* one-set segments with a rest between them — which is how you give each set its own game on the
|
|
207
|
+
* timeline — and adopting those numbers verbatim labels every set "set 1". Carrying the offset is
|
|
208
|
+
* what makes that authoring compile identically to the single three-set segment it replaces.
|
|
209
|
+
*/
|
|
210
|
+
let closedBefore = 0;
|
|
202
211
|
for (const entry of flat) {
|
|
203
212
|
if (entry.set !== undefined) {
|
|
204
|
-
// Authored by an interval set: adopt its numbering
|
|
205
|
-
set = entry.set;
|
|
213
|
+
// Authored by an interval set: adopt its numbering, shifted past any set already closed.
|
|
214
|
+
set = entry.set + closedBefore;
|
|
215
|
+
entry.set = set;
|
|
206
216
|
rep = entry.rep ?? rep;
|
|
207
217
|
// A rest emitted by a set closes it, exactly as it does below.
|
|
208
218
|
if (entry.type === "rest") {
|
|
@@ -227,6 +237,7 @@ function numberReps(flat) {
|
|
|
227
237
|
entry.set = set;
|
|
228
238
|
set += 1;
|
|
229
239
|
rep = 0;
|
|
240
|
+
closedBefore += 1;
|
|
230
241
|
}
|
|
231
242
|
}
|
|
232
243
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/training/compile.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/training/compile.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rydr/game-sdk",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/bdefrenne/rydr-game-sdk.git"
|
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
"./ui/showcase": {
|
|
64
64
|
"types": "./dist/ui/showcase/index.d.ts",
|
|
65
65
|
"import": "./dist/ui/showcase/index.js"
|
|
66
|
+
},
|
|
67
|
+
"./net": {
|
|
68
|
+
"types": "./dist/net/index.d.ts",
|
|
69
|
+
"import": "./dist/net/index.js"
|
|
66
70
|
}
|
|
67
71
|
},
|
|
68
72
|
"main": "./dist/index.js",
|
|
@@ -73,7 +77,7 @@
|
|
|
73
77
|
"scripts": {
|
|
74
78
|
"build": "npm run check:css && tsc -p tsconfig.json && npm run copy:docs",
|
|
75
79
|
"prepare": "npm run check:css && tsc -p tsconfig.json && npm run copy:docs",
|
|
76
|
-
"copy:docs": "cp src/ui/README.md dist/ui/README.md && cp src/three/README.md dist/three/README.md && cp src/characters/README.md dist/characters/README.md && cp src/nav/README.md dist/nav/README.md",
|
|
80
|
+
"copy:docs": "cp src/ui/README.md dist/ui/README.md && cp src/three/README.md dist/three/README.md && cp src/characters/README.md dist/characters/README.md && cp src/nav/README.md dist/nav/README.md && cp src/training/README.md dist/training/README.md",
|
|
77
81
|
"dev": "tsc -w -p tsconfig.json",
|
|
78
82
|
"clean": "rm -rf dist",
|
|
79
83
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
@@ -88,7 +92,8 @@
|
|
|
88
92
|
"check:css": "node scripts/check-css-literals.mjs",
|
|
89
93
|
"rig3d": "npm run build && vite examples/ui --open /rig3d.html",
|
|
90
94
|
"pad-hud": "npm run build && vite examples/ui --open /pad-hud.html",
|
|
91
|
-
"rig-hud": "npm run build && vite examples/ui --port 5180 --strictPort --open /rig-hud.html"
|
|
95
|
+
"rig-hud": "npm run build && vite examples/ui --port 5180 --strictPort --open /rig-hud.html",
|
|
96
|
+
"test": "node test/run.mjs"
|
|
92
97
|
},
|
|
93
98
|
"peerDependencies": {
|
|
94
99
|
"three": ">=0.160.0"
|
|
@@ -102,7 +107,8 @@
|
|
|
102
107
|
"@types/three": "^0.184.0",
|
|
103
108
|
"three": "^0.184.0",
|
|
104
109
|
"typescript": "^5.5.0",
|
|
105
|
-
"vite": "^6.0.0"
|
|
110
|
+
"vite": "^6.0.0",
|
|
111
|
+
"esbuild": "^0.28.2"
|
|
106
112
|
},
|
|
107
113
|
"dependencies": {
|
|
108
114
|
"@breezystack/lamejs": "^1.2.7"
|