@inventure71/paddockjs 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +146 -1
- package/INSTALL_AND_UPDATE.md +15 -4
- package/README.md +84 -562
- package/docs/architecture.md +28 -18
- package/docs/composable-layouts.md +77 -0
- package/docs/custom_model_controller.md +4 -4
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +9 -7
- package/docs/getting-started.md +75 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +17 -4
- package/docs/rules.md +6 -2
- package/docs/system_specs.md +49 -6
- package/docs/theming.md +64 -0
- package/docs/training.md +20 -14
- package/docs/troubleshooting.md +76 -0
- package/docs/upgrading-hosts-to-3.md +42 -0
- package/examples/python/README.md +6 -0
- package/examples/python/base_policy_server.py +19 -0
- package/package.json +10 -3
- package/src/api/PaddockSimulatorController.js +78 -22
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +24 -3
- package/src/app/readouts/timingTowerRenderer.js +203 -42
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/config/defaultOptions.js +8 -1
- package/src/config/themeOptions.js +5 -0
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/episode.js +11 -1
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/runtime.js +41 -0
- package/src/environment/sensors/carRays.js +48 -37
- package/src/environment/sensors/index.js +501 -97
- package/src/environment/sensors/indexedRayBands.js +134 -46
- package/src/environment/sensors/rayBandTrace.js +1171 -0
- package/src/environment/sensors/rayChannels.js +108 -0
- package/src/environment/sensors/rayConfig.js +71 -25
- package/src/environment/sensors/rayGeometry.js +23 -17
- package/src/environment/sensors/sensorTargets.js +67 -1
- package/src/environment/sensors/surfaceRays.js +130 -89
- package/src/environment/sensors/trackRays.js +115 -1
- package/src/index.d.ts +43 -0
- package/src/index.js +27 -2
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +77 -42
- package/src/simulation/pit/pitState.js +1 -0
- package/src/simulation/pit/pitTireService.js +7 -3
- package/src/simulation/race/raceFinish.js +25 -8
- package/src/simulation/race/raceOrder.js +189 -32
- package/src/simulation/race/raceProgress.js +129 -37
- package/src/simulation/race/raceSetup.js +2 -0
- package/src/simulation/race/raceStep.js +25 -13
- package/src/simulation/raceSimulation.js +20 -17
- package/src/simulation/rules/collisionSteward.js +40 -23
- package/src/simulation/rules/rulesReview.js +77 -56
- package/src/simulation/snapshots/raceSnapshots.js +37 -0
- package/src/simulation/timing/drsTiming.js +143 -30
- package/src/simulation/timing/gapEstimation.js +25 -3
- package/src/simulation/timing/lapTelemetry.js +115 -36
- package/src/simulation/timing/raceTiming.js +5 -1
- package/src/simulation/timing/sectorPerformance.js +104 -26
- package/src/simulation/timing/timingHistory.js +229 -14
- package/src/simulation/timing/timingLines.js +65 -7
- package/src/simulation/track/pitLaneLayout.js +23 -0
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +67 -11
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1334 -100
- package/src/simulation/track/trackQueryPitIndex.js +138 -14
- package/src/simulation/track/trackQueryScratch.js +7 -0
- package/src/simulation/track/trackQueryStats.js +25 -0
- package/src/simulation/track/trackSnapshotJson.js +139 -0
- package/src/simulation/track/trackStatePolicy.js +249 -1
- package/src/simulation/vehicle/contactResolution.js +96 -59
- package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
- package/src/simulation/vehicle/pitWheelSurface.js +81 -28
- package/src/simulation/vehicle/runoffResponse.js +54 -29
- package/src/simulation/vehicle/vehicleGeometry.js +245 -51
- package/src/simulation/vehicle/vehiclePhysics.js +42 -27
- package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
- package/src/simulation/vehicle/vehicleState.js +6 -0
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/styles.css +497 -497
|
@@ -12,7 +12,7 @@ function getSectorLength(track) {
|
|
|
12
12
|
return track.length / TELEMETRY_SECTOR_COUNT;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function
|
|
15
|
+
function writeLapTelemetryPosition(target, track, raceDistance, totalLaps = Infinity) {
|
|
16
16
|
const positiveDistance = Math.max(0, raceDistance ?? 0);
|
|
17
17
|
const lapIndex = Math.min(
|
|
18
18
|
Math.floor(positiveDistance / track.length),
|
|
@@ -25,42 +25,40 @@ function getLapTelemetryPosition(track, raceDistance, totalLaps = Infinity) {
|
|
|
25
25
|
const sectorIndex = Math.min(TELEMETRY_SECTOR_COUNT - 1, Math.floor(lapProgress / sectorLength));
|
|
26
26
|
const sectorStart = sectorIndex * sectorLength;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
};
|
|
28
|
+
target.completedLaps = Math.floor(positiveDistance / track.length);
|
|
29
|
+
target.currentLap = Math.max(1, lapIndex + 1);
|
|
30
|
+
target.currentSector = sectorIndex + 1;
|
|
31
|
+
target.currentSectorProgress = clamp((lapProgress - sectorStart) / sectorLength, 0, 1);
|
|
32
|
+
return target;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
function createSectorProgress(position, currentSectors = null, { preserveCompletedSectors = false } = {}) {
|
|
37
36
|
const activeIndex = clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (index === activeIndex) return clamp(position.currentSectorProgress ?? 0, 0, 1);
|
|
42
|
-
return 0;
|
|
43
|
-
});
|
|
37
|
+
const progress = createEmptySectorTimes();
|
|
38
|
+
writeSectorProgress(progress, position, currentSectors, { preserveCompletedSectors, activeIndex });
|
|
39
|
+
return progress;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
export function createLapTelemetry(track, currentTime = 0, raceDistance = 0, totalLaps = Infinity) {
|
|
47
|
-
const
|
|
48
|
-
return {
|
|
49
|
-
...position,
|
|
43
|
+
const telemetry = {
|
|
50
44
|
currentLapStartedAt: currentTime,
|
|
51
45
|
currentSectorStartedAt: currentTime,
|
|
52
46
|
lastUpdatedAt: currentTime,
|
|
53
47
|
currentLapTime: 0,
|
|
54
48
|
currentSectorElapsed: 0,
|
|
55
49
|
currentSectors: createEmptySectorTimes(),
|
|
56
|
-
sectorProgress:
|
|
50
|
+
sectorProgress: createEmptySectorTimes(),
|
|
57
51
|
liveSectors: createEmptySectorTimes(),
|
|
58
52
|
lastLapTime: null,
|
|
59
53
|
bestLapTime: null,
|
|
60
54
|
lastSectors: createEmptySectorTimes(),
|
|
61
55
|
bestSectors: createEmptySectorTimes(),
|
|
62
56
|
sectorPerformance: createEmptySectorPerformance(),
|
|
57
|
+
sectorPerformanceRevision: 0,
|
|
63
58
|
};
|
|
59
|
+
writeLapTelemetryPosition(telemetry, track, raceDistance, totalLaps);
|
|
60
|
+
telemetry.sectorProgress = createSectorProgress(telemetry);
|
|
61
|
+
return telemetry;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
export function resetLapTelemetry(car, currentTime, track, totalLaps) {
|
|
@@ -106,7 +104,7 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
|
|
|
106
104
|
Math.abs(travelled) > track.length / 2
|
|
107
105
|
) {
|
|
108
106
|
resetLapTelemetry(car, currentTime, track, totalLaps);
|
|
109
|
-
return;
|
|
107
|
+
return true;
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
if (travelled < -1e-3) {
|
|
@@ -114,10 +112,11 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
|
|
|
114
112
|
preserveCompletedSectors: true,
|
|
115
113
|
});
|
|
116
114
|
telemetry.lastUpdatedAt = currentTime;
|
|
117
|
-
return;
|
|
115
|
+
return false;
|
|
118
116
|
}
|
|
119
117
|
|
|
120
118
|
const sectorLength = getSectorLength(track);
|
|
119
|
+
let crossedBoundary = false;
|
|
121
120
|
if (travelled > 1e-6) {
|
|
122
121
|
const firstBoundary = Math.floor(previousDistance / sectorLength) + 1;
|
|
123
122
|
const lastBoundary = Math.floor(currentDistance / sectorLength);
|
|
@@ -125,6 +124,7 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
|
|
|
125
124
|
for (let boundary = firstBoundary; boundary <= lastBoundary; boundary += 1) {
|
|
126
125
|
const boundaryDistance = boundary * sectorLength;
|
|
127
126
|
if (boundaryDistance <= previousDistance + 1e-3 || boundaryDistance > currentDistance + 1e-3) continue;
|
|
127
|
+
crossedBoundary = true;
|
|
128
128
|
|
|
129
129
|
const boundaryRatio = clamp((boundaryDistance - previousDistance) / travelled, 0, 1);
|
|
130
130
|
const crossingTime = previousUpdateTime + boundaryRatio * elapsedTime;
|
|
@@ -140,8 +140,8 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
|
|
|
140
140
|
telemetry.bestLapTime = Number.isFinite(telemetry.bestLapTime)
|
|
141
141
|
? Math.min(telemetry.bestLapTime, lapTime)
|
|
142
142
|
: lapTime;
|
|
143
|
-
telemetry.lastSectors =
|
|
144
|
-
telemetry.currentSectors
|
|
143
|
+
telemetry.lastSectors = copySectorTimes(telemetry.lastSectors, telemetry.currentSectors);
|
|
144
|
+
clearSectorTimes(telemetry.currentSectors);
|
|
145
145
|
telemetry.currentLapStartedAt = crossingTime;
|
|
146
146
|
telemetry.completedLaps = Math.max(telemetry.completedLaps + 1, Math.min(Math.floor(boundaryDistance / track.length), totalLaps));
|
|
147
147
|
}
|
|
@@ -150,8 +150,18 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
if (!crossedBoundary) {
|
|
154
|
+
syncInProgressLapTelemetry(telemetry, currentTime, currentRaceDistance, track, totalLaps);
|
|
155
|
+
telemetry.lastUpdatedAt = currentTime;
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
153
159
|
syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps);
|
|
154
160
|
telemetry.lastUpdatedAt = currentTime;
|
|
161
|
+
if (crossedBoundary) {
|
|
162
|
+
telemetry.sectorPerformanceRevision = (telemetry.sectorPerformanceRevision ?? 0) + 1;
|
|
163
|
+
}
|
|
164
|
+
return crossedBoundary;
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
function updateBestSector(telemetry, sectorIndex, sectorTime) {
|
|
@@ -170,32 +180,101 @@ function clearFutureSectorTelemetry(telemetry) {
|
|
|
170
180
|
|
|
171
181
|
function syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors = false } = {}) {
|
|
172
182
|
const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
183
|
+
const liveSectors = ensureSectorTimesArray(telemetry.liveSectors);
|
|
184
|
+
clearSectorTimes(liveSectors);
|
|
185
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
186
|
+
const time = telemetry.currentSectors[index];
|
|
187
|
+
if (!preserveCompletedSectors && index >= activeIndex) continue;
|
|
188
|
+
if (!Number.isFinite(time)) continue;
|
|
189
|
+
liveSectors[index] = time;
|
|
190
|
+
}
|
|
191
|
+
if (!Number.isFinite(liveSectors[activeIndex])) {
|
|
192
|
+
liveSectors[activeIndex] = telemetry.currentSectorElapsed;
|
|
193
|
+
}
|
|
194
|
+
telemetry.liveSectors = liveSectors;
|
|
195
|
+
}
|
|
173
196
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
function syncInProgressLapTelemetry(telemetry, currentTime, currentRaceDistance, track, totalLaps) {
|
|
198
|
+
const positiveDistance = Math.max(0, currentRaceDistance ?? 0);
|
|
199
|
+
const maxLapIndex = Math.max(0, totalLaps - 1);
|
|
200
|
+
const lapIndex = Math.min(Math.floor(positiveDistance / track.length), maxLapIndex);
|
|
201
|
+
const lapProgress = positiveDistance >= track.length * totalLaps
|
|
202
|
+
? track.length
|
|
203
|
+
: positiveDistance - lapIndex * track.length;
|
|
204
|
+
const sectorLength = getSectorLength(track);
|
|
205
|
+
const activeIndex = Math.min(TELEMETRY_SECTOR_COUNT - 1, Math.floor(lapProgress / sectorLength));
|
|
206
|
+
const sectorStart = activeIndex * sectorLength;
|
|
207
|
+
const sectorProgress = ensureSectorTimesArray(telemetry.sectorProgress);
|
|
208
|
+
const liveSectors = ensureSectorTimesArray(telemetry.liveSectors);
|
|
209
|
+
|
|
210
|
+
telemetry.completedLaps = Math.floor(positiveDistance / track.length);
|
|
211
|
+
telemetry.currentLap = Math.max(1, lapIndex + 1);
|
|
212
|
+
telemetry.currentSector = activeIndex + 1;
|
|
213
|
+
telemetry.currentSectorProgress = clamp((lapProgress - sectorStart) / sectorLength, 0, 1);
|
|
214
|
+
telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
|
|
215
|
+
telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
|
|
216
|
+
for (let index = activeIndex; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
217
|
+
telemetry.currentSectors[index] = null;
|
|
218
|
+
liveSectors[index] = null;
|
|
219
|
+
sectorProgress[index] = 0;
|
|
182
220
|
}
|
|
221
|
+
sectorProgress[activeIndex] = telemetry.currentSectorProgress;
|
|
222
|
+
liveSectors[activeIndex] = telemetry.currentSectorElapsed;
|
|
223
|
+
telemetry.sectorProgress = sectorProgress;
|
|
224
|
+
telemetry.liveSectors = liveSectors;
|
|
183
225
|
}
|
|
184
226
|
|
|
185
227
|
function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
|
|
186
228
|
preserveCompletedSectors = false,
|
|
187
229
|
} = {}) {
|
|
188
|
-
|
|
189
|
-
telemetry.completedLaps = position.completedLaps;
|
|
190
|
-
telemetry.currentLap = position.currentLap;
|
|
191
|
-
telemetry.currentSector = position.currentSector;
|
|
192
|
-
telemetry.currentSectorProgress = position.currentSectorProgress;
|
|
230
|
+
writeLapTelemetryPosition(telemetry, track, currentRaceDistance, totalLaps);
|
|
193
231
|
if (!preserveCompletedSectors) clearFutureSectorTelemetry(telemetry);
|
|
194
|
-
|
|
232
|
+
const sectorProgress = ensureSectorTimesArray(telemetry.sectorProgress);
|
|
233
|
+
writeSectorProgress(sectorProgress, telemetry, telemetry.currentSectors, {
|
|
195
234
|
preserveCompletedSectors,
|
|
196
235
|
});
|
|
197
|
-
telemetry.
|
|
236
|
+
telemetry.sectorProgress = sectorProgress;
|
|
237
|
+
telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(telemetry.completedLaps, totalLaps));
|
|
198
238
|
telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
|
|
199
239
|
telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
|
|
200
240
|
syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors });
|
|
201
241
|
}
|
|
242
|
+
|
|
243
|
+
function ensureSectorTimesArray(values) {
|
|
244
|
+
return Array.isArray(values) && values.length === TELEMETRY_SECTOR_COUNT
|
|
245
|
+
? values
|
|
246
|
+
: createEmptySectorTimes();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function clearSectorTimes(values) {
|
|
250
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
251
|
+
values[index] = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function copySectorTimes(target, source) {
|
|
256
|
+
const nextTarget = ensureSectorTimesArray(target);
|
|
257
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
258
|
+
nextTarget[index] = finiteOrNull(source?.[index]);
|
|
259
|
+
}
|
|
260
|
+
return nextTarget;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function writeSectorProgress(target, position, currentSectors = null, { preserveCompletedSectors = false, activeIndex = null } = {}) {
|
|
264
|
+
const resolvedActiveIndex = activeIndex == null
|
|
265
|
+
? clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1)
|
|
266
|
+
: activeIndex;
|
|
267
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
268
|
+
if (preserveCompletedSectors && Number.isFinite(currentSectors?.[index])) {
|
|
269
|
+
target[index] = 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (index < resolvedActiveIndex && Number.isFinite(currentSectors?.[index])) {
|
|
273
|
+
target[index] = 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
target[index] = index === resolvedActiveIndex
|
|
277
|
+
? clamp(position.currentSectorProgress ?? 0, 0, 1)
|
|
278
|
+
: 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -5,7 +5,11 @@ export {
|
|
|
5
5
|
updateLapTelemetry,
|
|
6
6
|
} from './lapTelemetry.js';
|
|
7
7
|
export { estimateGapAheadSeconds, estimateTimingLineGapSeconds, wholeLapGap } from './gapEstimation.js';
|
|
8
|
-
export {
|
|
8
|
+
export {
|
|
9
|
+
createEmptySectorPerformance,
|
|
10
|
+
createEmptySectorTimes,
|
|
11
|
+
updateSectorPerformance,
|
|
12
|
+
} from './sectorPerformance.js';
|
|
9
13
|
export { recordTimingSample, resetTimingHistory, trimTimingHistory } from './timingHistory.js';
|
|
10
14
|
export {
|
|
11
15
|
createTimingLines,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { TELEMETRY_SECTOR_COUNT } from './timingConstants.js';
|
|
2
2
|
|
|
3
|
+
const OVERALL_BEST_0 = '_sectorPerformanceOverallBest0';
|
|
4
|
+
const OVERALL_BEST_1 = '_sectorPerformanceOverallBest1';
|
|
5
|
+
const OVERALL_BEST_2 = '_sectorPerformanceOverallBest2';
|
|
6
|
+
|
|
3
7
|
export function createEmptySectorTimes() {
|
|
4
8
|
return Array.from({ length: TELEMETRY_SECTOR_COUNT }, () => null);
|
|
5
9
|
}
|
|
@@ -20,32 +24,80 @@ export function serializeSectorPerformance(values) {
|
|
|
20
24
|
return createEmptySectorTimes().map((_, index) => values?.[index] ?? null);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
export function updateSectorPerformance(cars) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
telemetry.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
27
|
+
export function updateSectorPerformance(cars, stats = null) {
|
|
28
|
+
let overallBest0 = null;
|
|
29
|
+
let overallBest1 = null;
|
|
30
|
+
let overallBest2 = null;
|
|
31
|
+
let changedCars = 0;
|
|
32
|
+
|
|
33
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
34
|
+
const telemetry = cars[index]?.lapTelemetry;
|
|
35
|
+
if (!telemetry) continue;
|
|
36
|
+
const best0 = finiteOrNull(telemetry.bestSectors?.[0]);
|
|
37
|
+
const best1 = finiteOrNull(telemetry.bestSectors?.[1]);
|
|
38
|
+
const best2 = finiteOrNull(telemetry.bestSectors?.[2]);
|
|
39
|
+
const revision = telemetry.sectorPerformanceRevision ?? 0;
|
|
40
|
+
if (telemetry._sectorPerfAppliedRevision !== revision) changedCars += 1;
|
|
41
|
+
if (Number.isFinite(best0) && (!Number.isFinite(overallBest0) || best0 < overallBest0)) overallBest0 = best0;
|
|
42
|
+
if (Number.isFinite(best1) && (!Number.isFinite(overallBest1) || best1 < overallBest1)) overallBest1 = best1;
|
|
43
|
+
if (Number.isFinite(best2) && (!Number.isFinite(overallBest2) || best2 < overallBest2)) overallBest2 = best2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const overallBestChanged =
|
|
47
|
+
overallBest0 !== cars[OVERALL_BEST_0] ||
|
|
48
|
+
overallBest1 !== cars[OVERALL_BEST_1] ||
|
|
49
|
+
overallBest2 !== cars[OVERALL_BEST_2];
|
|
50
|
+
|
|
51
|
+
if (stats) {
|
|
52
|
+
stats.sectorPerformanceChangedCars = (stats.sectorPerformanceChangedCars ?? 0) + changedCars;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!overallBestChanged && changedCars === 0) {
|
|
56
|
+
if (stats) stats.sectorPerformanceSkippedRebuilds = (stats.sectorPerformanceSkippedRebuilds ?? 0) + 1;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cars[OVERALL_BEST_0] = overallBest0;
|
|
61
|
+
cars[OVERALL_BEST_1] = overallBest1;
|
|
62
|
+
cars[OVERALL_BEST_2] = overallBest2;
|
|
63
|
+
|
|
64
|
+
let updatedCars = 0;
|
|
65
|
+
const overallBestSectors = [overallBest0, overallBest1, overallBest2];
|
|
66
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
67
|
+
const telemetry = cars[index]?.lapTelemetry;
|
|
68
|
+
if (!telemetry) continue;
|
|
69
|
+
const revision = telemetry.sectorPerformanceRevision ?? 0;
|
|
70
|
+
if (!overallBestChanged && telemetry._sectorPerfAppliedRevision === revision) continue;
|
|
71
|
+
const sectorPerformance = ensureSectorPerformanceShape(telemetry.sectorPerformance);
|
|
72
|
+
writeSectorPerformance(
|
|
73
|
+
sectorPerformance.current,
|
|
74
|
+
telemetry.currentSectors,
|
|
75
|
+
telemetry.bestSectors,
|
|
76
|
+
overallBestSectors,
|
|
77
|
+
);
|
|
78
|
+
writeSectorPerformance(
|
|
79
|
+
sectorPerformance.last,
|
|
80
|
+
telemetry.lastSectors,
|
|
81
|
+
telemetry.bestSectors,
|
|
82
|
+
overallBestSectors,
|
|
83
|
+
);
|
|
84
|
+
writeSectorPerformance(
|
|
85
|
+
sectorPerformance.best,
|
|
86
|
+
telemetry.bestSectors,
|
|
87
|
+
telemetry.bestSectors,
|
|
88
|
+
overallBestSectors,
|
|
89
|
+
);
|
|
90
|
+
telemetry.sectorPerformance = sectorPerformance;
|
|
91
|
+
telemetry._sectorPerfAppliedRevision = revision;
|
|
92
|
+
updatedCars += 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (stats) {
|
|
96
|
+
stats.sectorPerformanceRebuilds = (stats.sectorPerformanceRebuilds ?? 0) + 1;
|
|
97
|
+
stats.sectorPerformanceUpdatedCars = (stats.sectorPerformanceUpdatedCars ?? 0) + updatedCars;
|
|
98
|
+
if (overallBestChanged) stats.sectorPerformanceOverallBestChanges = (stats.sectorPerformanceOverallBestChanges ?? 0) + 1;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
49
101
|
}
|
|
50
102
|
|
|
51
103
|
export function finiteOrNull(value) {
|
|
@@ -58,3 +110,29 @@ function classifySectorPerformance(value, personalBest, overallBest) {
|
|
|
58
110
|
if (Number.isFinite(personalBest) && Math.abs(value - personalBest) <= 1e-6) return 'personal-best';
|
|
59
111
|
return 'slower';
|
|
60
112
|
}
|
|
113
|
+
|
|
114
|
+
function ensureSectorPerformanceShape(sectorPerformance) {
|
|
115
|
+
if (!sectorPerformance || typeof sectorPerformance !== 'object') {
|
|
116
|
+
return createEmptySectorPerformance();
|
|
117
|
+
}
|
|
118
|
+
sectorPerformance.current = ensureSectorArray(sectorPerformance.current);
|
|
119
|
+
sectorPerformance.last = ensureSectorArray(sectorPerformance.last);
|
|
120
|
+
sectorPerformance.best = ensureSectorArray(sectorPerformance.best);
|
|
121
|
+
return sectorPerformance;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureSectorArray(values) {
|
|
125
|
+
return Array.isArray(values) && values.length === TELEMETRY_SECTOR_COUNT
|
|
126
|
+
? values
|
|
127
|
+
: createEmptySectorTimes();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function writeSectorPerformance(target, sectorValues, bestSectors, overallBestSectors) {
|
|
131
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
132
|
+
target[index] = classifySectorPerformance(
|
|
133
|
+
sectorValues?.[index],
|
|
134
|
+
bestSectors?.[index],
|
|
135
|
+
overallBestSectors[index],
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -1,39 +1,218 @@
|
|
|
1
1
|
import { TIMING_HISTORY_MAX_SAMPLES, TIMING_HISTORY_WINDOW_SECONDS } from './timingConstants.js';
|
|
2
2
|
|
|
3
|
+
const TIMING_HISTORY_START_INDEX = '_startIndex';
|
|
4
|
+
const TIMING_HISTORY_COUNT = '_count';
|
|
5
|
+
const TIMING_HISTORY_INTERPOLATION_OFFSET = '_interpolationOffset';
|
|
6
|
+
const TIMING_HISTORY_INTERPOLATION_LOWER_DISTANCE = '_interpolationLowerDistance';
|
|
7
|
+
const TIMING_HISTORY_INTERPOLATION_UPPER_DISTANCE = '_interpolationUpperDistance';
|
|
8
|
+
const TIMING_HISTORY_INTERPOLATION_LOWER_TIME = '_interpolationLowerTime';
|
|
9
|
+
const TIMING_HISTORY_INTERPOLATION_UPPER_TIME = '_interpolationUpperTime';
|
|
10
|
+
const TIMING_HISTORY_INTERPOLATION_STATS = '_interpolationStats';
|
|
11
|
+
|
|
12
|
+
function isStructuredTimingHistory(history) {
|
|
13
|
+
return Boolean(
|
|
14
|
+
history &&
|
|
15
|
+
history.times instanceof Float64Array &&
|
|
16
|
+
history.distances instanceof Float64Array,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createStructuredTimingHistory(currentTime, raceDistance) {
|
|
21
|
+
const history = {
|
|
22
|
+
times: new Float64Array(TIMING_HISTORY_MAX_SAMPLES),
|
|
23
|
+
distances: new Float64Array(TIMING_HISTORY_MAX_SAMPLES),
|
|
24
|
+
};
|
|
25
|
+
history.times[0] = currentTime;
|
|
26
|
+
history.distances[0] = raceDistance;
|
|
27
|
+
history[TIMING_HISTORY_START_INDEX] = 0;
|
|
28
|
+
history[TIMING_HISTORY_COUNT] = 1;
|
|
29
|
+
return history;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getTimingHistoryStartIndex(history) {
|
|
33
|
+
if (isStructuredTimingHistory(history)) {
|
|
34
|
+
const value = history[TIMING_HISTORY_START_INDEX];
|
|
35
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
36
|
+
}
|
|
37
|
+
const value = history?.[TIMING_HISTORY_START_INDEX];
|
|
38
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setTimingHistoryStartIndex(history, startIndex) {
|
|
42
|
+
if (isStructuredTimingHistory(history)) {
|
|
43
|
+
history[TIMING_HISTORY_START_INDEX] = startIndex;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
Object.defineProperty(history, TIMING_HISTORY_START_INDEX, {
|
|
47
|
+
value: startIndex,
|
|
48
|
+
writable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
enumerable: false,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clearTimingHistoryStartIndex(history) {
|
|
55
|
+
if (isStructuredTimingHistory(history)) {
|
|
56
|
+
history[TIMING_HISTORY_START_INDEX] = 0;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
delete history[TIMING_HISTORY_START_INDEX];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function activeTimingHistoryLength(history, startIndex = getTimingHistoryStartIndex(history)) {
|
|
63
|
+
if (isStructuredTimingHistory(history)) {
|
|
64
|
+
const count = history[TIMING_HISTORY_COUNT];
|
|
65
|
+
return Number.isInteger(count) && count >= 0 ? count : 0;
|
|
66
|
+
}
|
|
67
|
+
return Math.max(0, history.length - startIndex);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function compactTimingHistory(history, startIndex) {
|
|
71
|
+
if (isStructuredTimingHistory(history)) {
|
|
72
|
+
history[TIMING_HISTORY_START_INDEX] = startIndex;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (startIndex <= 0) {
|
|
76
|
+
clearTimingHistoryStartIndex(history);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
history.splice(0, startIndex);
|
|
80
|
+
clearTimingHistoryStartIndex(history);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function trimStructuredTimingHistory(history, currentTime) {
|
|
84
|
+
let startIndex = getTimingHistoryStartIndex(history);
|
|
85
|
+
let count = activeTimingHistoryLength(history, startIndex);
|
|
86
|
+
const cutoff = currentTime - TIMING_HISTORY_WINDOW_SECONDS;
|
|
87
|
+
while (
|
|
88
|
+
count > 2 &&
|
|
89
|
+
history.times[startIndex] < cutoff
|
|
90
|
+
) {
|
|
91
|
+
startIndex = (startIndex + 1) % TIMING_HISTORY_MAX_SAMPLES;
|
|
92
|
+
count -= 1;
|
|
93
|
+
}
|
|
94
|
+
history[TIMING_HISTORY_START_INDEX] = startIndex;
|
|
95
|
+
history[TIMING_HISTORY_COUNT] = count;
|
|
96
|
+
}
|
|
97
|
+
|
|
3
98
|
export function trimTimingHistory(history, currentTime) {
|
|
99
|
+
if (isStructuredTimingHistory(history)) {
|
|
100
|
+
trimStructuredTimingHistory(history, currentTime);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(history)) return;
|
|
4
104
|
const cutoff = currentTime - TIMING_HISTORY_WINDOW_SECONDS;
|
|
5
|
-
|
|
6
|
-
|
|
105
|
+
let startIndex = getTimingHistoryStartIndex(history);
|
|
106
|
+
if (
|
|
107
|
+
startIndex === 0 &&
|
|
108
|
+
history.length <= TIMING_HISTORY_MAX_SAMPLES &&
|
|
109
|
+
history.length <= 2
|
|
110
|
+
) return;
|
|
111
|
+
if (
|
|
112
|
+
startIndex === 0 &&
|
|
113
|
+
history.length <= TIMING_HISTORY_MAX_SAMPLES &&
|
|
114
|
+
history[0]?.time >= cutoff
|
|
115
|
+
) return;
|
|
116
|
+
while (
|
|
117
|
+
activeTimingHistoryLength(history, startIndex) > 2 &&
|
|
118
|
+
(
|
|
119
|
+
history[startIndex]?.time < cutoff ||
|
|
120
|
+
activeTimingHistoryLength(history, startIndex) > TIMING_HISTORY_MAX_SAMPLES
|
|
121
|
+
)
|
|
122
|
+
) {
|
|
123
|
+
startIndex += 1;
|
|
7
124
|
}
|
|
125
|
+
if (startIndex > 0 && (startIndex >= 64 || startIndex * 2 >= history.length)) {
|
|
126
|
+
compactTimingHistory(history, startIndex);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (startIndex > 0) setTimingHistoryStartIndex(history, startIndex);
|
|
130
|
+
else clearTimingHistoryStartIndex(history);
|
|
8
131
|
}
|
|
9
132
|
|
|
10
133
|
export function resetTimingHistory(car, currentTime) {
|
|
11
|
-
car.timingHistory =
|
|
134
|
+
car.timingHistory = createStructuredTimingHistory(currentTime, car.raceDistance);
|
|
12
135
|
}
|
|
13
136
|
|
|
14
137
|
export function recordTimingSample(car, currentTime) {
|
|
15
138
|
if (!Number.isFinite(car.raceDistance)) return;
|
|
16
|
-
if (!
|
|
139
|
+
if (!isStructuredTimingHistory(car.timingHistory)) {
|
|
17
140
|
resetTimingHistory(car, currentTime);
|
|
18
|
-
return;
|
|
19
141
|
}
|
|
20
|
-
|
|
21
|
-
const
|
|
142
|
+
const history = car.timingHistory;
|
|
143
|
+
const startIndex = getTimingHistoryStartIndex(history);
|
|
144
|
+
const count = activeTimingHistoryLength(history, startIndex);
|
|
145
|
+
const previousIndex = count > 0
|
|
146
|
+
? (startIndex + count - 1) % TIMING_HISTORY_MAX_SAMPLES
|
|
147
|
+
: 0;
|
|
148
|
+
const previousTime = count > 0 ? history.times[previousIndex] : null;
|
|
149
|
+
const previousRaceDistance = count > 0 ? history.distances[previousIndex] : null;
|
|
22
150
|
if (
|
|
23
|
-
|
|
24
|
-
Math.abs(
|
|
25
|
-
Math.abs(
|
|
151
|
+
count > 0 &&
|
|
152
|
+
Math.abs(previousTime - currentTime) <= 1e-6 &&
|
|
153
|
+
Math.abs(previousRaceDistance - car.raceDistance) <= 1e-3
|
|
26
154
|
) {
|
|
27
155
|
return;
|
|
28
156
|
}
|
|
29
157
|
|
|
30
|
-
|
|
31
|
-
|
|
158
|
+
let nextStartIndex = startIndex;
|
|
159
|
+
let nextCount = count;
|
|
160
|
+
let writeIndex = 0;
|
|
161
|
+
if (nextCount < TIMING_HISTORY_MAX_SAMPLES) {
|
|
162
|
+
writeIndex = (nextStartIndex + nextCount) % TIMING_HISTORY_MAX_SAMPLES;
|
|
163
|
+
nextCount += 1;
|
|
164
|
+
} else {
|
|
165
|
+
writeIndex = nextStartIndex;
|
|
166
|
+
nextStartIndex = (nextStartIndex + 1) % TIMING_HISTORY_MAX_SAMPLES;
|
|
167
|
+
}
|
|
168
|
+
history.times[writeIndex] = currentTime;
|
|
169
|
+
history.distances[writeIndex] = car.raceDistance;
|
|
170
|
+
history[TIMING_HISTORY_START_INDEX] = nextStartIndex;
|
|
171
|
+
history[TIMING_HISTORY_COUNT] = nextCount;
|
|
172
|
+
trimStructuredTimingHistory(history, currentTime);
|
|
32
173
|
}
|
|
33
174
|
|
|
34
175
|
export function interpolateTimeAtDistance(history, targetDistance) {
|
|
35
|
-
if (
|
|
36
|
-
|
|
176
|
+
if (isStructuredTimingHistory(history)) {
|
|
177
|
+
const directCachedTime = interpolateDirectCachedSegment(history, targetDistance);
|
|
178
|
+
if (Number.isFinite(directCachedTime)) return directCachedTime;
|
|
179
|
+
const startIndex = getTimingHistoryStartIndex(history);
|
|
180
|
+
const count = activeTimingHistoryLength(history, startIndex);
|
|
181
|
+
if (count < 2) return null;
|
|
182
|
+
const cachedOffset = history[TIMING_HISTORY_INTERPOLATION_OFFSET];
|
|
183
|
+
if (Number.isInteger(cachedOffset) && cachedOffset >= 0 && cachedOffset < count - 1) {
|
|
184
|
+
const cachedTime = interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, cachedOffset);
|
|
185
|
+
if (Number.isFinite(cachedTime)) return cachedTime;
|
|
186
|
+
const resolvedOffset = history[TIMING_HISTORY_INTERPOLATION_OFFSET];
|
|
187
|
+
if (Number.isInteger(resolvedOffset) && resolvedOffset >= 0 && resolvedOffset < count - 1) {
|
|
188
|
+
const currentPosition = count - 1 - resolvedOffset;
|
|
189
|
+
const currentIndex = (startIndex + currentPosition) % TIMING_HISTORY_MAX_SAMPLES;
|
|
190
|
+
const previousIndex = (startIndex + currentPosition - 1) % TIMING_HISTORY_MAX_SAMPLES;
|
|
191
|
+
const currentDistance = history.distances[currentIndex];
|
|
192
|
+
const previousDistance = history.distances[previousIndex];
|
|
193
|
+
if (targetDistance > currentDistance) {
|
|
194
|
+
for (let offset = resolvedOffset - 1; offset >= 0; offset -= 1) {
|
|
195
|
+
const value = interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, offset);
|
|
196
|
+
if (Number.isFinite(value)) return value;
|
|
197
|
+
}
|
|
198
|
+
} else if (targetDistance < previousDistance) {
|
|
199
|
+
for (let offset = resolvedOffset + 1; offset < count - 1; offset += 1) {
|
|
200
|
+
const value = interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, offset);
|
|
201
|
+
if (Number.isFinite(value)) return value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (let offset = 0; offset < count - 1; offset += 1) {
|
|
207
|
+
const value = interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, offset);
|
|
208
|
+
if (Number.isFinite(value)) return value;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (!Array.isArray(history)) return null;
|
|
213
|
+
const startIndex = getTimingHistoryStartIndex(history);
|
|
214
|
+
if (activeTimingHistoryLength(history, startIndex) < 2) return null;
|
|
215
|
+
for (let index = history.length - 1; index > startIndex; index -= 1) {
|
|
37
216
|
const current = history[index];
|
|
38
217
|
const previous = history[index - 1];
|
|
39
218
|
if (targetDistance < previous.raceDistance || targetDistance > current.raceDistance) continue;
|
|
@@ -44,3 +223,39 @@ export function interpolateTimeAtDistance(history, targetDistance) {
|
|
|
44
223
|
}
|
|
45
224
|
return null;
|
|
46
225
|
}
|
|
226
|
+
|
|
227
|
+
function interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, offsetFromTail) {
|
|
228
|
+
const currentPosition = count - 1 - offsetFromTail;
|
|
229
|
+
const currentIndex = (startIndex + currentPosition) % TIMING_HISTORY_MAX_SAMPLES;
|
|
230
|
+
const previousIndex = (startIndex + currentPosition - 1) % TIMING_HISTORY_MAX_SAMPLES;
|
|
231
|
+
const currentDistance = history.distances[currentIndex];
|
|
232
|
+
const previousDistance = history.distances[previousIndex];
|
|
233
|
+
if (targetDistance < previousDistance || targetDistance > currentDistance) return null;
|
|
234
|
+
history[TIMING_HISTORY_INTERPOLATION_OFFSET] = offsetFromTail;
|
|
235
|
+
history[TIMING_HISTORY_INTERPOLATION_LOWER_DISTANCE] = previousDistance;
|
|
236
|
+
history[TIMING_HISTORY_INTERPOLATION_UPPER_DISTANCE] = currentDistance;
|
|
237
|
+
history[TIMING_HISTORY_INTERPOLATION_LOWER_TIME] = history.times[previousIndex];
|
|
238
|
+
history[TIMING_HISTORY_INTERPOLATION_UPPER_TIME] = history.times[currentIndex];
|
|
239
|
+
const coveredDistance = currentDistance - previousDistance;
|
|
240
|
+
if (coveredDistance <= 1e-6) return history.times[currentIndex];
|
|
241
|
+
const ratio = (targetDistance - previousDistance) / coveredDistance;
|
|
242
|
+
return history.times[previousIndex] + (history.times[currentIndex] - history.times[previousIndex]) * ratio;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function interpolateDirectCachedSegment(history, targetDistance) {
|
|
246
|
+
const lowerDistance = history[TIMING_HISTORY_INTERPOLATION_LOWER_DISTANCE];
|
|
247
|
+
const upperDistance = history[TIMING_HISTORY_INTERPOLATION_UPPER_DISTANCE];
|
|
248
|
+
if (!Number.isFinite(lowerDistance) || !Number.isFinite(upperDistance)) return null;
|
|
249
|
+
if (targetDistance < lowerDistance || targetDistance > upperDistance) return null;
|
|
250
|
+
|
|
251
|
+
const lowerTime = history[TIMING_HISTORY_INTERPOLATION_LOWER_TIME];
|
|
252
|
+
const upperTime = history[TIMING_HISTORY_INTERPOLATION_UPPER_TIME];
|
|
253
|
+
const stats = history[TIMING_HISTORY_INTERPOLATION_STATS];
|
|
254
|
+
if (stats) {
|
|
255
|
+
stats.directSegmentCacheHits = (stats.directSegmentCacheHits ?? 0) + 1;
|
|
256
|
+
}
|
|
257
|
+
const coveredDistance = upperDistance - lowerDistance;
|
|
258
|
+
if (coveredDistance <= 1e-6) return upperTime;
|
|
259
|
+
const ratio = (targetDistance - lowerDistance) / coveredDistance;
|
|
260
|
+
return lowerTime + (upperTime - lowerTime) * ratio;
|
|
261
|
+
}
|