@inventure71/paddockjs 2.0.0 → 2.0.1
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/CHANGELOG.md +6 -0
- package/INSTALL_AND_UPDATE.md +1 -1
- package/README.md +30 -5
- package/docs/architecture.md +15 -9
- package/docs/concepts.md +6 -4
- package/docs/data_contract.md +28 -15
- package/docs/data_model.md +1 -1
- package/docs/index.md +9 -0
- package/docs/rules.md +37 -7
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +56 -7
- package/docs/training.md +16 -10
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +2 -33
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +1 -42
- package/src/config/restartOptions.js +34 -0
- package/src/environment/controllerLoop.js +17 -2
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +16 -2
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +62 -14
- package/src/environment/scenarios.js +22 -3
- package/src/environment/sensors/index.js +3 -3
- package/src/environment/sensors/opponentRadar.js +13 -3
- package/src/environment/sensors/sensorTargets.js +8 -2
- package/src/environment/sensors/surfaceRays.js +90 -26
- package/src/environment/sensors/trackRays.js +185 -22
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -32
- package/src/simulation/pit/pitFlow.js +5 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitQueue.js +5 -0
- package/src/simulation/pit/pitRouteMovement.js +7 -0
- package/src/simulation/pit/pitService.js +2 -0
- package/src/simulation/pit/pitTireService.js +2 -0
- package/src/simulation/race/raceLifecycle.js +2 -6
- package/src/simulation/race/raceProgress.js +6 -1
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/redFlag.js +3 -3
- package/src/simulation/race/safetyCar.js +10 -4
- package/src/simulation/race/stalledDnf.js +8 -17
- package/src/simulation/raceSimulation.js +1 -0
- package/src/simulation/rules/rulesReview.js +3 -0
- package/src/simulation/snapshots/raceSnapshots.js +1 -0
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +16 -302
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/vehicle/contactResolution.js +67 -2
- package/src/simulation/vehicle/runoffResponse.js +11 -12
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +2 -1
- package/src/simulation/vehicle/vehicleSnapshots.js +11 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { createEnvironmentRuntime } from '../environment/runtime.js';
|
|
2
2
|
import { resolveEnvironmentOptions } from '../environment/options.js';
|
|
3
|
-
import {
|
|
3
|
+
import { createBrowserExpertExternalRendererBridge } from './BrowserExpertExternalRenderer.js';
|
|
4
4
|
|
|
5
5
|
export function createBrowserExpertAdapter(app, expertOptions = {}) {
|
|
6
6
|
let resolvedOptions = resolveBrowserExpertOptions(app.options, expertOptions);
|
|
7
7
|
let frameRenderSuppressed = false;
|
|
8
|
-
|
|
9
|
-
let externalRendererAttached = false;
|
|
10
|
-
let externalRendererLastMeta = null;
|
|
11
|
-
let externalRendererLastFrameAt = null;
|
|
12
|
-
let externalRendererLastError = null;
|
|
13
|
-
let externalRendererDriverIdMap = new Map();
|
|
14
|
-
let externalRendererTrackFingerprint = null;
|
|
15
|
-
let externalRendererAppliedTrackSurface = false;
|
|
8
|
+
const externalRenderer = createBrowserExpertExternalRendererBridge(app);
|
|
16
9
|
|
|
17
10
|
function resolveBrowserExpertOptions(appOptions, nextExpertOptions = {}) {
|
|
18
11
|
const options = resolveEnvironmentOptions({
|
|
@@ -53,215 +46,6 @@ export function createBrowserExpertAdapter(app, expertOptions = {}) {
|
|
|
53
46
|
|
|
54
47
|
resolvedOptions = initialResolvedOptions();
|
|
55
48
|
|
|
56
|
-
function localDriverRecords() {
|
|
57
|
-
const source = Array.isArray(app.drivers) && app.drivers.length > 0
|
|
58
|
-
? app.drivers
|
|
59
|
-
: app.options?.drivers;
|
|
60
|
-
return Array.isArray(source) ? source : [];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function localDriverIds() {
|
|
64
|
-
return localDriverRecords()
|
|
65
|
-
.map((driver) => driver?.id)
|
|
66
|
-
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function localDriverById() {
|
|
70
|
-
return new Map(localDriverRecords().map((driver) => [driver.id, driver]));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function rewriteExternalFrame(frame) {
|
|
74
|
-
if (!frame || typeof frame !== 'object') return frame;
|
|
75
|
-
const snapshot = frame.snapshot;
|
|
76
|
-
if (!snapshot || typeof snapshot !== 'object' || !Array.isArray(snapshot.cars)) return frame;
|
|
77
|
-
|
|
78
|
-
const knownLocalIds = localDriverIds();
|
|
79
|
-
if (knownLocalIds.length === 0) return frame;
|
|
80
|
-
const knownLocalIdSet = new Set(knownLocalIds);
|
|
81
|
-
|
|
82
|
-
const activeRawIds = new Set();
|
|
83
|
-
const usedLocalIds = new Set();
|
|
84
|
-
|
|
85
|
-
snapshot.cars.forEach((car, index) => {
|
|
86
|
-
const rawId = typeof car?.id === 'string' && car.id.length > 0
|
|
87
|
-
? car.id
|
|
88
|
-
: `external-${String(index)}`;
|
|
89
|
-
activeRawIds.add(rawId);
|
|
90
|
-
if (knownLocalIdSet.has(rawId) && !usedLocalIds.has(rawId)) {
|
|
91
|
-
externalRendererDriverIdMap.set(rawId, rawId);
|
|
92
|
-
usedLocalIds.add(rawId);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const mapped = externalRendererDriverIdMap.get(rawId);
|
|
96
|
-
if (typeof mapped === 'string' && mapped.length > 0 && !usedLocalIds.has(mapped)) {
|
|
97
|
-
usedLocalIds.add(mapped);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const nextLocalId = knownLocalIds.find((id) => !usedLocalIds.has(id));
|
|
101
|
-
if (nextLocalId) {
|
|
102
|
-
externalRendererDriverIdMap.set(rawId, nextLocalId);
|
|
103
|
-
usedLocalIds.add(nextLocalId);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
externalRendererDriverIdMap.set(rawId, rawId);
|
|
107
|
-
usedLocalIds.add(rawId);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
for (const rawId of Array.from(externalRendererDriverIdMap.keys())) {
|
|
111
|
-
if (!activeRawIds.has(rawId)) externalRendererDriverIdMap.delete(rawId);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const mapId = (value) => {
|
|
115
|
-
if (typeof value !== 'string' || value.length === 0) return value;
|
|
116
|
-
return externalRendererDriverIdMap.get(value) ?? value;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const drivers = localDriverById();
|
|
120
|
-
const mappedCars = snapshot.cars.map((car, index) => {
|
|
121
|
-
const rawId = typeof car?.id === 'string' && car.id.length > 0
|
|
122
|
-
? car.id
|
|
123
|
-
: `external-${String(index)}`;
|
|
124
|
-
const mappedId = mapId(rawId);
|
|
125
|
-
const driver = drivers.get(mappedId);
|
|
126
|
-
return {
|
|
127
|
-
...car,
|
|
128
|
-
id: mappedId,
|
|
129
|
-
code: car?.code ?? driver?.code ?? mappedId,
|
|
130
|
-
timingCode: car?.timingCode ?? driver?.timingCode ?? driver?.code ?? mappedId,
|
|
131
|
-
name: car?.name ?? driver?.name ?? mappedId,
|
|
132
|
-
color: car?.color ?? driver?.color ?? '#9ca3af',
|
|
133
|
-
icon: car?.icon ?? driver?.icon ?? (driver?.code ?? mappedId),
|
|
134
|
-
team: car?.team ?? driver?.team ?? null,
|
|
135
|
-
};
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const mappedEvents = Array.isArray(snapshot.events)
|
|
139
|
-
? snapshot.events.map((event) => ({
|
|
140
|
-
...event,
|
|
141
|
-
carId: mapId(event?.carId),
|
|
142
|
-
otherCarId: mapId(event?.otherCarId),
|
|
143
|
-
winnerId: mapId(event?.winnerId),
|
|
144
|
-
}))
|
|
145
|
-
: snapshot.events;
|
|
146
|
-
|
|
147
|
-
const mappedPenalties = Array.isArray(snapshot.penalties)
|
|
148
|
-
? snapshot.penalties.map((penalty) => ({
|
|
149
|
-
...penalty,
|
|
150
|
-
driverId: mapId(penalty?.driverId),
|
|
151
|
-
}))
|
|
152
|
-
: snapshot.penalties;
|
|
153
|
-
|
|
154
|
-
const raceControl = snapshot.raceControl && typeof snapshot.raceControl === 'object'
|
|
155
|
-
? {
|
|
156
|
-
...snapshot.raceControl,
|
|
157
|
-
winner: snapshot.raceControl.winner && typeof snapshot.raceControl.winner === 'object'
|
|
158
|
-
? {
|
|
159
|
-
...snapshot.raceControl.winner,
|
|
160
|
-
id: mapId(snapshot.raceControl.winner.id),
|
|
161
|
-
}
|
|
162
|
-
: snapshot.raceControl.winner,
|
|
163
|
-
classification: Array.isArray(snapshot.raceControl.classification)
|
|
164
|
-
? snapshot.raceControl.classification.map((entry) => ({
|
|
165
|
-
...entry,
|
|
166
|
-
id: mapId(entry?.id),
|
|
167
|
-
driverId: mapId(entry?.driverId),
|
|
168
|
-
}))
|
|
169
|
-
: snapshot.raceControl.classification,
|
|
170
|
-
}
|
|
171
|
-
: snapshot.raceControl;
|
|
172
|
-
|
|
173
|
-
let mappedObservation = frame.observation;
|
|
174
|
-
if (frame.observation && typeof frame.observation === 'object' && !Array.isArray(frame.observation)) {
|
|
175
|
-
mappedObservation = {};
|
|
176
|
-
Object.entries(frame.observation).forEach(([driverId, value]) => {
|
|
177
|
-
mappedObservation[mapId(driverId)] = value;
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
...frame,
|
|
183
|
-
snapshot: {
|
|
184
|
-
...snapshot,
|
|
185
|
-
cars: mappedCars,
|
|
186
|
-
events: mappedEvents,
|
|
187
|
-
penalties: mappedPenalties,
|
|
188
|
-
raceControl,
|
|
189
|
-
},
|
|
190
|
-
observation: mappedObservation,
|
|
191
|
-
meta: {
|
|
192
|
-
...(frame.meta && typeof frame.meta === 'object' ? frame.meta : {}),
|
|
193
|
-
externalDriverMap: Object.fromEntries(externalRendererDriverIdMap.entries()),
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function trackFingerprint(snapshot) {
|
|
199
|
-
const track = snapshot?.track;
|
|
200
|
-
if (!track || typeof track !== 'object') return null;
|
|
201
|
-
const drs = Array.isArray(track.drsZones)
|
|
202
|
-
? track.drsZones.map((zone) => [zone?.id ?? null, zone?.start ?? null, zone?.end ?? null])
|
|
203
|
-
: [];
|
|
204
|
-
const firstSample = Array.isArray(track.samples) && track.samples.length > 0 ? track.samples[0] : null;
|
|
205
|
-
const lastSample = Array.isArray(track.samples) && track.samples.length > 0
|
|
206
|
-
? track.samples[track.samples.length - 1]
|
|
207
|
-
: null;
|
|
208
|
-
return JSON.stringify({
|
|
209
|
-
name: track.name ?? null,
|
|
210
|
-
length: track.length ?? null,
|
|
211
|
-
width: track.width ?? null,
|
|
212
|
-
sampleCount: track.sampleCount ?? null,
|
|
213
|
-
finish: track.finish
|
|
214
|
-
? {
|
|
215
|
-
x: track.finish.x ?? null,
|
|
216
|
-
y: track.finish.y ?? null,
|
|
217
|
-
heading: track.finish.heading ?? null,
|
|
218
|
-
}
|
|
219
|
-
: null,
|
|
220
|
-
firstSample: firstSample
|
|
221
|
-
? {
|
|
222
|
-
x: firstSample.x ?? null,
|
|
223
|
-
y: firstSample.y ?? null,
|
|
224
|
-
heading: firstSample.heading ?? null,
|
|
225
|
-
}
|
|
226
|
-
: null,
|
|
227
|
-
lastSample: lastSample
|
|
228
|
-
? {
|
|
229
|
-
x: lastSample.x ?? null,
|
|
230
|
-
y: lastSample.y ?? null,
|
|
231
|
-
heading: lastSample.heading ?? null,
|
|
232
|
-
}
|
|
233
|
-
: null,
|
|
234
|
-
drs,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function syncExternalTrackSurface(snapshot) {
|
|
239
|
-
if (!snapshot || typeof snapshot !== 'object') return;
|
|
240
|
-
const nextFingerprint = trackFingerprint(snapshot);
|
|
241
|
-
if (!nextFingerprint) return;
|
|
242
|
-
if (nextFingerprint === externalRendererTrackFingerprint) return;
|
|
243
|
-
if (
|
|
244
|
-
!app.trackAsset ||
|
|
245
|
-
!app.drsLayer ||
|
|
246
|
-
!app.sensorLayer ||
|
|
247
|
-
!app.pitLaneStatusLayer
|
|
248
|
-
) {
|
|
249
|
-
externalRendererTrackFingerprint = nextFingerprint;
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
app.pitLaneStatusRenderer?.reset?.();
|
|
253
|
-
app.cameraController?.invalidateTrackCaches?.();
|
|
254
|
-
renderTrackSurface({
|
|
255
|
-
drsLayer: app.drsLayer,
|
|
256
|
-
sensorLayer: app.sensorLayer,
|
|
257
|
-
pitLaneStatusLayer: app.pitLaneStatusLayer,
|
|
258
|
-
trackAsset: app.trackAsset,
|
|
259
|
-
snapshot,
|
|
260
|
-
});
|
|
261
|
-
externalRendererTrackFingerprint = nextFingerprint;
|
|
262
|
-
externalRendererAppliedTrackSurface = true;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
49
|
const runtime = createEnvironmentRuntime({
|
|
266
50
|
getSimulation: () => app.sim,
|
|
267
51
|
setSimulation(nextSim) {
|
|
@@ -287,104 +71,29 @@ export function createBrowserExpertAdapter(app, expertOptions = {}) {
|
|
|
287
71
|
},
|
|
288
72
|
});
|
|
289
73
|
|
|
290
|
-
function assertExternalRendererDetached(method) {
|
|
291
|
-
if (!externalRendererAttached) return;
|
|
292
|
-
throw new Error(`Browser expert ${method}() is disabled while external renderer mode is attached.`);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function detachExternalRenderer() {
|
|
296
|
-
if (typeof externalRendererUnsubscribe === 'function') {
|
|
297
|
-
try {
|
|
298
|
-
externalRendererUnsubscribe();
|
|
299
|
-
} catch (error) {
|
|
300
|
-
externalRendererLastError = error instanceof Error ? error.message : String(error);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
externalRendererUnsubscribe = null;
|
|
304
|
-
externalRendererAttached = false;
|
|
305
|
-
externalRendererDriverIdMap = new Map();
|
|
306
|
-
if (externalRendererAppliedTrackSurface && typeof app.renderTrack === 'function') {
|
|
307
|
-
try {
|
|
308
|
-
app.renderTrack();
|
|
309
|
-
} catch (error) {
|
|
310
|
-
externalRendererLastError = error instanceof Error ? error.message : String(error);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
externalRendererTrackFingerprint = null;
|
|
314
|
-
externalRendererAppliedTrackSurface = false;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function attachExternalRenderer(source) {
|
|
318
|
-
if (!source || typeof source.subscribe !== 'function') {
|
|
319
|
-
throw new Error('attachExternalRenderer(source) requires a source with subscribe(onFrame).');
|
|
320
|
-
}
|
|
321
|
-
detachExternalRenderer();
|
|
322
|
-
externalRendererLastError = null;
|
|
323
|
-
const unsubscribe = source.subscribe((frame) => {
|
|
324
|
-
if (!frame || typeof frame !== 'object') {
|
|
325
|
-
externalRendererLastError = 'Invalid external frame payload.';
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const rewrittenFrame = rewriteExternalFrame(frame);
|
|
329
|
-
const snapshot = rewrittenFrame.snapshot;
|
|
330
|
-
if (!snapshot || typeof snapshot !== 'object') {
|
|
331
|
-
externalRendererLastError = 'External frame is missing snapshot.';
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
try {
|
|
335
|
-
syncExternalTrackSurface(snapshot);
|
|
336
|
-
app.renderExpertFrame(snapshot, {
|
|
337
|
-
forceDomUpdate: true,
|
|
338
|
-
observation: rewrittenFrame.observation && typeof rewrittenFrame.observation === 'object'
|
|
339
|
-
? rewrittenFrame.observation
|
|
340
|
-
: {},
|
|
341
|
-
});
|
|
342
|
-
externalRendererLastMeta = rewrittenFrame.meta ?? null;
|
|
343
|
-
externalRendererLastFrameAt = Date.now();
|
|
344
|
-
externalRendererLastError = null;
|
|
345
|
-
} catch (error) {
|
|
346
|
-
externalRendererLastError = error instanceof Error ? error.message : String(error);
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
if (typeof unsubscribe !== 'function') {
|
|
350
|
-
throw new Error('attachExternalRenderer(source) subscribe(onFrame) must return an unsubscribe function.');
|
|
351
|
-
}
|
|
352
|
-
externalRendererUnsubscribe = unsubscribe;
|
|
353
|
-
externalRendererAttached = true;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function getExternalRendererState() {
|
|
357
|
-
return {
|
|
358
|
-
attached: externalRendererAttached,
|
|
359
|
-
lastMeta: externalRendererLastMeta,
|
|
360
|
-
lastFrameAt: externalRendererLastFrameAt,
|
|
361
|
-
lastError: externalRendererLastError,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
74
|
return {
|
|
366
75
|
...runtime,
|
|
367
76
|
reset(options = {}) {
|
|
368
|
-
|
|
77
|
+
externalRenderer.assertDetached('reset');
|
|
369
78
|
return runtime.reset(options);
|
|
370
79
|
},
|
|
371
80
|
step(actions = {}) {
|
|
372
|
-
|
|
81
|
+
externalRenderer.assertDetached('step');
|
|
373
82
|
return runtime.step(actions);
|
|
374
83
|
},
|
|
375
84
|
resetDrivers(placements = {}, resultOptions = {}) {
|
|
376
|
-
|
|
85
|
+
externalRenderer.assertDetached('resetDrivers');
|
|
377
86
|
return runtime.resetDrivers(placements, resultOptions);
|
|
378
87
|
},
|
|
379
88
|
destroy() {
|
|
380
|
-
|
|
89
|
+
externalRenderer.detach();
|
|
381
90
|
runtime.destroy();
|
|
382
91
|
},
|
|
383
92
|
setFrameRenderSuppressed(suppressed) {
|
|
384
93
|
frameRenderSuppressed = Boolean(suppressed);
|
|
385
94
|
},
|
|
386
|
-
attachExternalRenderer,
|
|
387
|
-
detachExternalRenderer,
|
|
388
|
-
getExternalRendererState,
|
|
95
|
+
attachExternalRenderer: externalRenderer.attach,
|
|
96
|
+
detachExternalRenderer: externalRenderer.detach,
|
|
97
|
+
getExternalRendererState: externalRenderer.getState,
|
|
389
98
|
};
|
|
390
99
|
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { renderTrackSurface } from './rendering/trackRenderer.js';
|
|
2
|
+
|
|
3
|
+
export function createBrowserExpertExternalRendererBridge(app) {
|
|
4
|
+
let unsubscribeExternalRenderer = null;
|
|
5
|
+
let attached = false;
|
|
6
|
+
let lastMeta = null;
|
|
7
|
+
let lastFrameAt = null;
|
|
8
|
+
let lastError = null;
|
|
9
|
+
let driverIdMap = new Map();
|
|
10
|
+
let trackFingerprint = null;
|
|
11
|
+
let appliedTrackSurface = false;
|
|
12
|
+
|
|
13
|
+
function localDriverRecords() {
|
|
14
|
+
const source = Array.isArray(app.drivers) && app.drivers.length > 0
|
|
15
|
+
? app.drivers
|
|
16
|
+
: app.options?.drivers;
|
|
17
|
+
return Array.isArray(source) ? source : [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function localDriverIds() {
|
|
21
|
+
return localDriverRecords()
|
|
22
|
+
.map((driver) => driver?.id)
|
|
23
|
+
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function localDriverById() {
|
|
27
|
+
return new Map(localDriverRecords().map((driver) => [driver.id, driver]));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rewriteFrame(frame) {
|
|
31
|
+
if (!frame || typeof frame !== 'object') return frame;
|
|
32
|
+
const snapshot = frame.snapshot;
|
|
33
|
+
if (!snapshot || typeof snapshot !== 'object' || !Array.isArray(snapshot.cars)) return frame;
|
|
34
|
+
|
|
35
|
+
const knownLocalIds = localDriverIds();
|
|
36
|
+
if (knownLocalIds.length === 0) return frame;
|
|
37
|
+
const knownLocalIdSet = new Set(knownLocalIds);
|
|
38
|
+
|
|
39
|
+
const activeRawIds = new Set();
|
|
40
|
+
const usedLocalIds = new Set();
|
|
41
|
+
|
|
42
|
+
snapshot.cars.forEach((car, index) => {
|
|
43
|
+
const rawId = typeof car?.id === 'string' && car.id.length > 0
|
|
44
|
+
? car.id
|
|
45
|
+
: `external-${String(index)}`;
|
|
46
|
+
activeRawIds.add(rawId);
|
|
47
|
+
if (knownLocalIdSet.has(rawId) && !usedLocalIds.has(rawId)) {
|
|
48
|
+
driverIdMap.set(rawId, rawId);
|
|
49
|
+
usedLocalIds.add(rawId);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const mapped = driverIdMap.get(rawId);
|
|
53
|
+
if (typeof mapped === 'string' && mapped.length > 0 && !usedLocalIds.has(mapped)) {
|
|
54
|
+
usedLocalIds.add(mapped);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const nextLocalId = knownLocalIds.find((id) => !usedLocalIds.has(id));
|
|
58
|
+
if (nextLocalId) {
|
|
59
|
+
driverIdMap.set(rawId, nextLocalId);
|
|
60
|
+
usedLocalIds.add(nextLocalId);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
driverIdMap.set(rawId, rawId);
|
|
64
|
+
usedLocalIds.add(rawId);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
for (const rawId of Array.from(driverIdMap.keys())) {
|
|
68
|
+
if (!activeRawIds.has(rawId)) driverIdMap.delete(rawId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mapId = (value) => {
|
|
72
|
+
if (typeof value !== 'string' || value.length === 0) return value;
|
|
73
|
+
return driverIdMap.get(value) ?? value;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const drivers = localDriverById();
|
|
77
|
+
const mappedCars = snapshot.cars.map((car, index) => {
|
|
78
|
+
const rawId = typeof car?.id === 'string' && car.id.length > 0
|
|
79
|
+
? car.id
|
|
80
|
+
: `external-${String(index)}`;
|
|
81
|
+
const mappedId = mapId(rawId);
|
|
82
|
+
const driver = drivers.get(mappedId);
|
|
83
|
+
return {
|
|
84
|
+
...car,
|
|
85
|
+
id: mappedId,
|
|
86
|
+
code: car?.code ?? driver?.code ?? mappedId,
|
|
87
|
+
timingCode: car?.timingCode ?? driver?.timingCode ?? driver?.code ?? mappedId,
|
|
88
|
+
name: car?.name ?? driver?.name ?? mappedId,
|
|
89
|
+
color: car?.color ?? driver?.color ?? '#9ca3af',
|
|
90
|
+
icon: car?.icon ?? driver?.icon ?? (driver?.code ?? mappedId),
|
|
91
|
+
team: car?.team ?? driver?.team ?? null,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const mappedEvents = Array.isArray(snapshot.events)
|
|
96
|
+
? snapshot.events.map((event) => ({
|
|
97
|
+
...event,
|
|
98
|
+
carId: mapId(event?.carId),
|
|
99
|
+
otherCarId: mapId(event?.otherCarId),
|
|
100
|
+
winnerId: mapId(event?.winnerId),
|
|
101
|
+
}))
|
|
102
|
+
: snapshot.events;
|
|
103
|
+
|
|
104
|
+
const mappedPenalties = Array.isArray(snapshot.penalties)
|
|
105
|
+
? snapshot.penalties.map((penalty) => ({
|
|
106
|
+
...penalty,
|
|
107
|
+
driverId: mapId(penalty?.driverId),
|
|
108
|
+
}))
|
|
109
|
+
: snapshot.penalties;
|
|
110
|
+
|
|
111
|
+
const raceControl = snapshot.raceControl && typeof snapshot.raceControl === 'object'
|
|
112
|
+
? {
|
|
113
|
+
...snapshot.raceControl,
|
|
114
|
+
winner: snapshot.raceControl.winner && typeof snapshot.raceControl.winner === 'object'
|
|
115
|
+
? {
|
|
116
|
+
...snapshot.raceControl.winner,
|
|
117
|
+
id: mapId(snapshot.raceControl.winner.id),
|
|
118
|
+
}
|
|
119
|
+
: snapshot.raceControl.winner,
|
|
120
|
+
classification: Array.isArray(snapshot.raceControl.classification)
|
|
121
|
+
? snapshot.raceControl.classification.map((entry) => ({
|
|
122
|
+
...entry,
|
|
123
|
+
id: mapId(entry?.id),
|
|
124
|
+
driverId: mapId(entry?.driverId),
|
|
125
|
+
}))
|
|
126
|
+
: snapshot.raceControl.classification,
|
|
127
|
+
}
|
|
128
|
+
: snapshot.raceControl;
|
|
129
|
+
|
|
130
|
+
let mappedObservation = frame.observation;
|
|
131
|
+
if (frame.observation && typeof frame.observation === 'object' && !Array.isArray(frame.observation)) {
|
|
132
|
+
mappedObservation = {};
|
|
133
|
+
Object.entries(frame.observation).forEach(([driverId, value]) => {
|
|
134
|
+
mappedObservation[mapId(driverId)] = value;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...frame,
|
|
140
|
+
snapshot: {
|
|
141
|
+
...snapshot,
|
|
142
|
+
cars: mappedCars,
|
|
143
|
+
events: mappedEvents,
|
|
144
|
+
penalties: mappedPenalties,
|
|
145
|
+
raceControl,
|
|
146
|
+
},
|
|
147
|
+
observation: mappedObservation,
|
|
148
|
+
meta: {
|
|
149
|
+
...(frame.meta && typeof frame.meta === 'object' ? frame.meta : {}),
|
|
150
|
+
externalDriverMap: Object.fromEntries(driverIdMap.entries()),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function fingerprintTrack(snapshot) {
|
|
156
|
+
const track = snapshot?.track;
|
|
157
|
+
if (!track || typeof track !== 'object') return null;
|
|
158
|
+
const drs = Array.isArray(track.drsZones)
|
|
159
|
+
? track.drsZones.map((zone) => [zone?.id ?? null, zone?.start ?? null, zone?.end ?? null])
|
|
160
|
+
: [];
|
|
161
|
+
const firstSample = Array.isArray(track.samples) && track.samples.length > 0 ? track.samples[0] : null;
|
|
162
|
+
const lastSample = Array.isArray(track.samples) && track.samples.length > 0
|
|
163
|
+
? track.samples[track.samples.length - 1]
|
|
164
|
+
: null;
|
|
165
|
+
return JSON.stringify({
|
|
166
|
+
name: track.name ?? null,
|
|
167
|
+
length: track.length ?? null,
|
|
168
|
+
width: track.width ?? null,
|
|
169
|
+
sampleCount: track.sampleCount ?? null,
|
|
170
|
+
finish: track.finish
|
|
171
|
+
? {
|
|
172
|
+
x: track.finish.x ?? null,
|
|
173
|
+
y: track.finish.y ?? null,
|
|
174
|
+
heading: track.finish.heading ?? null,
|
|
175
|
+
}
|
|
176
|
+
: null,
|
|
177
|
+
firstSample: firstSample
|
|
178
|
+
? {
|
|
179
|
+
x: firstSample.x ?? null,
|
|
180
|
+
y: firstSample.y ?? null,
|
|
181
|
+
heading: firstSample.heading ?? null,
|
|
182
|
+
}
|
|
183
|
+
: null,
|
|
184
|
+
lastSample: lastSample
|
|
185
|
+
? {
|
|
186
|
+
x: lastSample.x ?? null,
|
|
187
|
+
y: lastSample.y ?? null,
|
|
188
|
+
heading: lastSample.heading ?? null,
|
|
189
|
+
}
|
|
190
|
+
: null,
|
|
191
|
+
drs,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function syncTrackSurface(snapshot) {
|
|
196
|
+
if (!snapshot || typeof snapshot !== 'object') return;
|
|
197
|
+
const nextFingerprint = fingerprintTrack(snapshot);
|
|
198
|
+
if (!nextFingerprint) return;
|
|
199
|
+
if (nextFingerprint === trackFingerprint) return;
|
|
200
|
+
if (
|
|
201
|
+
!app.trackAsset ||
|
|
202
|
+
!app.drsLayer ||
|
|
203
|
+
!app.sensorLayer ||
|
|
204
|
+
!app.pitLaneStatusLayer
|
|
205
|
+
) {
|
|
206
|
+
trackFingerprint = nextFingerprint;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
app.pitLaneStatusRenderer?.reset?.();
|
|
210
|
+
app.cameraController?.invalidateTrackCaches?.();
|
|
211
|
+
renderTrackSurface({
|
|
212
|
+
drsLayer: app.drsLayer,
|
|
213
|
+
sensorLayer: app.sensorLayer,
|
|
214
|
+
pitLaneStatusLayer: app.pitLaneStatusLayer,
|
|
215
|
+
trackAsset: app.trackAsset,
|
|
216
|
+
snapshot,
|
|
217
|
+
});
|
|
218
|
+
trackFingerprint = nextFingerprint;
|
|
219
|
+
appliedTrackSurface = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function assertDetached(method) {
|
|
223
|
+
if (!attached) return;
|
|
224
|
+
throw new Error(`Browser expert ${method}() is disabled while external renderer mode is attached.`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function detach() {
|
|
228
|
+
if (typeof unsubscribeExternalRenderer === 'function') {
|
|
229
|
+
try {
|
|
230
|
+
unsubscribeExternalRenderer();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
unsubscribeExternalRenderer = null;
|
|
236
|
+
attached = false;
|
|
237
|
+
driverIdMap = new Map();
|
|
238
|
+
if (appliedTrackSurface && typeof app.renderTrack === 'function') {
|
|
239
|
+
try {
|
|
240
|
+
app.renderTrack();
|
|
241
|
+
} catch (error) {
|
|
242
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
trackFingerprint = null;
|
|
246
|
+
appliedTrackSurface = false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function attach(source) {
|
|
250
|
+
if (!source || typeof source.subscribe !== 'function') {
|
|
251
|
+
throw new Error('attachExternalRenderer(source) requires a source with subscribe(onFrame).');
|
|
252
|
+
}
|
|
253
|
+
detach();
|
|
254
|
+
lastError = null;
|
|
255
|
+
const unsubscribe = source.subscribe((frame) => {
|
|
256
|
+
if (!frame || typeof frame !== 'object') {
|
|
257
|
+
lastError = 'Invalid external frame payload.';
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const rewrittenFrame = rewriteFrame(frame);
|
|
261
|
+
const snapshot = rewrittenFrame.snapshot;
|
|
262
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
263
|
+
lastError = 'External frame is missing snapshot.';
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
syncTrackSurface(snapshot);
|
|
268
|
+
app.renderExpertFrame(snapshot, {
|
|
269
|
+
forceDomUpdate: true,
|
|
270
|
+
observation: rewrittenFrame.observation && typeof rewrittenFrame.observation === 'object'
|
|
271
|
+
? rewrittenFrame.observation
|
|
272
|
+
: {},
|
|
273
|
+
});
|
|
274
|
+
lastMeta = rewrittenFrame.meta ?? null;
|
|
275
|
+
lastFrameAt = Date.now();
|
|
276
|
+
lastError = null;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
if (typeof unsubscribe !== 'function') {
|
|
282
|
+
throw new Error('attachExternalRenderer(source) subscribe(onFrame) must return an unsubscribe function.');
|
|
283
|
+
}
|
|
284
|
+
unsubscribeExternalRenderer = unsubscribe;
|
|
285
|
+
attached = true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getState() {
|
|
289
|
+
return {
|
|
290
|
+
attached,
|
|
291
|
+
lastMeta,
|
|
292
|
+
lastFrameAt,
|
|
293
|
+
lastError,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
assertDetached,
|
|
299
|
+
attach,
|
|
300
|
+
detach,
|
|
301
|
+
getState,
|
|
302
|
+
};
|
|
303
|
+
}
|