@inventure71/paddockjs 3.0.0 → 4.1.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/INSTALL_AND_UPDATE.md +21 -4
  3. package/README.md +103 -551
  4. package/docs/architecture.md +28 -18
  5. package/docs/component_inventory.md +5 -4
  6. package/docs/composable-layouts.md +79 -0
  7. package/docs/custom_model_controller.md +4 -4
  8. package/docs/data-helpers.md +50 -0
  9. package/docs/data_contract.md +13 -7
  10. package/docs/getting-started.md +105 -0
  11. package/docs/headless-environment.md +68 -0
  12. package/docs/index.md +17 -4
  13. package/docs/rules.md +6 -2
  14. package/docs/system_specs.md +60 -7
  15. package/docs/theming.md +64 -0
  16. package/docs/training.md +20 -14
  17. package/docs/troubleshooting.md +76 -0
  18. package/docs/upgrading-hosts-to-3.md +42 -0
  19. package/examples/python/README.md +6 -0
  20. package/examples/python/base_policy_server.py +19 -0
  21. package/package.json +21 -5
  22. package/src/api/PaddockSimulatorController.js +78 -22
  23. package/src/api/runtimeTheme.js +59 -0
  24. package/src/app/F1SimulatorApp.js +24 -3
  25. package/src/app/readouts/timingTowerRenderer.js +203 -42
  26. package/src/app/runtime/frameLoop.js +5 -1
  27. package/src/config/defaultOptions.js +8 -1
  28. package/src/config/themeOptions.js +5 -0
  29. package/src/data/index.d.ts +212 -0
  30. package/src/data/index.js +14 -0
  31. package/src/environment/episode.js +11 -1
  32. package/src/environment/metrics.js +59 -16
  33. package/src/environment/observationVector.js +50 -33
  34. package/src/environment/observations.js +89 -20
  35. package/src/environment/runtime.js +41 -0
  36. package/src/environment/sensors/carRays.js +48 -37
  37. package/src/environment/sensors/index.js +501 -97
  38. package/src/environment/sensors/indexedRayBands.js +134 -46
  39. package/src/environment/sensors/rayBandTrace.js +1171 -0
  40. package/src/environment/sensors/rayChannels.js +108 -0
  41. package/src/environment/sensors/rayConfig.js +71 -25
  42. package/src/environment/sensors/rayGeometry.js +23 -17
  43. package/src/environment/sensors/sensorTargets.js +67 -1
  44. package/src/environment/sensors/surfaceRays.js +130 -89
  45. package/src/environment/sensors/trackRays.js +115 -1
  46. package/src/index.d.ts +43 -0
  47. package/src/index.js +27 -2
  48. package/src/placeholder/index.d.ts +11 -0
  49. package/src/placeholder/index.js +61 -0
  50. package/src/placeholder.css +72 -0
  51. package/src/simulation/collisionGeometry.js +260 -96
  52. package/src/simulation/pit/pitFlow.js +21 -9
  53. package/src/simulation/pit/pitQueue.js +5 -3
  54. package/src/simulation/pit/pitRouteMovement.js +9 -4
  55. package/src/simulation/pit/pitRouting.js +77 -42
  56. package/src/simulation/pit/pitState.js +1 -0
  57. package/src/simulation/pit/pitTireService.js +7 -3
  58. package/src/simulation/race/raceFinish.js +25 -8
  59. package/src/simulation/race/raceOrder.js +189 -32
  60. package/src/simulation/race/raceProgress.js +129 -37
  61. package/src/simulation/race/raceSetup.js +2 -0
  62. package/src/simulation/race/raceStep.js +25 -13
  63. package/src/simulation/raceSimulation.js +20 -17
  64. package/src/simulation/rules/collisionSteward.js +40 -23
  65. package/src/simulation/rules/rulesReview.js +77 -56
  66. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  67. package/src/simulation/timing/drsTiming.js +143 -30
  68. package/src/simulation/timing/gapEstimation.js +25 -3
  69. package/src/simulation/timing/lapTelemetry.js +115 -36
  70. package/src/simulation/timing/raceTiming.js +5 -1
  71. package/src/simulation/timing/sectorPerformance.js +104 -26
  72. package/src/simulation/timing/timingHistory.js +229 -14
  73. package/src/simulation/timing/timingLines.js +65 -7
  74. package/src/simulation/track/pitLaneLayout.js +23 -0
  75. package/src/simulation/track/pitLaneState.js +218 -19
  76. package/src/simulation/track/spatialQueries.js +67 -11
  77. package/src/simulation/track/trackMath.js +91 -24
  78. package/src/simulation/track/trackModel.js +2 -0
  79. package/src/simulation/track/trackQueryIndex.js +1334 -100
  80. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  81. package/src/simulation/track/trackQueryScratch.js +7 -0
  82. package/src/simulation/track/trackQueryStats.js +25 -0
  83. package/src/simulation/track/trackSnapshotJson.js +139 -0
  84. package/src/simulation/track/trackStatePolicy.js +249 -1
  85. package/src/simulation/vehicle/contactResolution.js +96 -59
  86. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  87. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  88. package/src/simulation/vehicle/runoffResponse.js +54 -29
  89. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  90. package/src/simulation/vehicle/vehiclePhysics.js +42 -27
  91. package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
  92. package/src/simulation/vehicle/vehicleState.js +6 -0
  93. package/src/simulation/vehicle/wheelSurface.js +807 -92
  94. package/src/styles.css +497 -497
  95. package/src/ui/raceControlsTemplate.js +1 -0
  96. package/src/ui/telemetryDrawerTemplate.js +1 -0
  97. package/src/ui/telemetryTemplates.js +1 -0
@@ -1,4 +1,4 @@
1
- import { calculateCollisionPenalties } from './collisionSteward.js';
1
+ import { emitCollisionPenalties } from './collisionSteward.js';
2
2
  import { calculateTireRequirementPenalty } from './tireRequirementSteward.js';
3
3
  import { calculateTrackLimitReview } from './trackLimitsSteward.js';
4
4
  import { calculatePitLaneSpeedingReview } from './pitLaneSpeedingSteward.js';
@@ -6,25 +6,16 @@ import { getPenaltyRule } from '../rulesConfig.js';
6
6
  import { simSpeedToKph } from '../units.js';
7
7
  import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
8
8
 
9
- function forwardVector(car) {
10
- return { x: Math.cos(car.heading), y: Math.sin(car.heading) };
9
+ function velocityComponentX(car) {
10
+ return Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
11
+ ? car.velocityX
12
+ : Math.cos(car.heading) * car.speed;
11
13
  }
12
14
 
13
- function velocityVector(car) {
14
- if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
15
- return { x: car.velocityX, y: car.velocityY };
16
- }
17
- const forward = forwardVector(car);
18
- return { x: forward.x * car.speed, y: forward.y * car.speed };
19
- }
20
-
21
- function dot(a, b) {
22
- return a.x * b.x + a.y * b.y;
23
- }
24
-
25
- function normalizeVector(vector) {
26
- const length = Math.hypot(vector.x, vector.y) || 1;
27
- return { x: vector.x / length, y: vector.y / length };
15
+ function velocityComponentY(car) {
16
+ return Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
17
+ ? car.velocityY
18
+ : Math.sin(car.heading) * car.speed;
28
19
  }
29
20
 
30
21
  function progressDelta(a, b, trackLength) {
@@ -34,48 +25,62 @@ function progressDelta(a, b, trackLength) {
34
25
  return delta;
35
26
  }
36
27
 
37
- function createCollisionStewardContext(first, second, collision) {
38
- const distanceDelta = collision.trackLength
39
- ? progressDelta(second.progress ?? second.raceDistance ?? 0, first.progress ?? first.raceDistance ?? 0, collision.trackLength)
28
+ function prepareCollisionStewardContext(scratch = null) {
29
+ const target = scratch
30
+ ? (scratch.collisionStewardContext ??= { sharedFaultDriverIds: [] })
31
+ : { sharedFaultDriverIds: [] };
32
+ target.sharedFaultDriverIds ??= [];
33
+ return target;
34
+ }
35
+
36
+ function writeCollisionFacts(target, collision, trackLength) {
37
+ target.depth = collision?.depth ?? 0;
38
+ target.trackLength = trackLength ?? collision?.trackLength ?? null;
39
+ target.firstShapeId = collision?.firstShapeId ?? null;
40
+ target.secondShapeId = collision?.secondShapeId ?? null;
41
+ target.contactType = collision?.contactType ?? null;
42
+ target.timeOfImpact = collision?.timeOfImpact ?? null;
43
+ target.swept = Boolean(collision?.swept);
44
+ target.axis = collision?.axis ?? null;
45
+ target.impactSpeed = 0;
46
+ target.aheadDriverId = null;
47
+ target.atFaultDriverId = null;
48
+ target.sharedFault = false;
49
+ target.sharedFaultDriverIds.length = 0;
50
+ return target;
51
+ }
52
+
53
+ function writeCollisionStewardContext(target, first, second, collision, trackLength = null) {
54
+ writeCollisionFacts(target, collision, trackLength);
55
+ const distanceDelta = target.trackLength
56
+ ? progressDelta(second.progress ?? second.raceDistance ?? 0, first.progress ?? first.raceDistance ?? 0, target.trackLength)
40
57
  : (second.raceDistance ?? 0) - (first.raceDistance ?? 0);
41
58
  const sideBySideTolerance = VEHICLE_LIMITS.carLength * 0.18;
42
59
  if (Math.abs(distanceDelta) <= sideBySideTolerance) {
43
- const firstVelocity = velocityVector(first);
44
- const secondVelocity = velocityVector(second);
45
- const relativeVelocity = {
46
- x: firstVelocity.x - secondVelocity.x,
47
- y: firstVelocity.y - secondVelocity.y,
48
- };
49
- return {
50
- ...collision,
51
- impactSpeed: Math.hypot(relativeVelocity.x, relativeVelocity.y),
52
- aheadDriverId: null,
53
- atFaultDriverId: null,
54
- sharedFault: true,
55
- sharedFaultDriverIds: [first.id, second.id],
56
- };
60
+ const relativeVelocityX = velocityComponentX(first) - velocityComponentX(second);
61
+ const relativeVelocityY = velocityComponentY(first) - velocityComponentY(second);
62
+ target.impactSpeed = Math.hypot(relativeVelocityX, relativeVelocityY);
63
+ target.sharedFault = true;
64
+ target.sharedFaultDriverIds[0] = first.id;
65
+ target.sharedFaultDriverIds[1] = second.id;
66
+ target.sharedFaultDriverIds.length = 2;
67
+ return target;
57
68
  }
58
69
 
59
70
  const firstBehind = distanceDelta > 0;
60
71
  const behind = firstBehind ? first : second;
61
72
  const ahead = firstBehind ? second : first;
62
- const directionBehindToAhead = normalizeVector({
63
- x: ahead.x - behind.x,
64
- y: ahead.y - behind.y,
65
- });
66
- const behindVelocity = velocityVector(behind);
67
- const aheadVelocity = velocityVector(ahead);
68
- const relativeVelocity = {
69
- x: behindVelocity.x - aheadVelocity.x,
70
- y: behindVelocity.y - aheadVelocity.y,
71
- };
72
-
73
- return {
74
- ...collision,
75
- impactSpeed: Math.max(0, dot(relativeVelocity, directionBehindToAhead)),
76
- aheadDriverId: ahead.id,
77
- atFaultDriverId: behind.id,
78
- };
73
+ const directionX = ahead.x - behind.x;
74
+ const directionY = ahead.y - behind.y;
75
+ const directionLength = Math.hypot(directionX, directionY) || 1;
76
+ const normalX = directionX / directionLength;
77
+ const normalY = directionY / directionLength;
78
+ const relativeVelocityX = velocityComponentX(behind) - velocityComponentX(ahead);
79
+ const relativeVelocityY = velocityComponentY(behind) - velocityComponentY(ahead);
80
+ target.impactSpeed = Math.max(0, relativeVelocityX * normalX + relativeVelocityY * normalY);
81
+ target.aheadDriverId = ahead.id;
82
+ target.atFaultDriverId = behind.id;
83
+ return target;
79
84
  }
80
85
 
81
86
  function isLegallyInsidePitLaneForTrackLimits(car) {
@@ -84,14 +89,30 @@ function isLegallyInsidePitLaneForTrackLimits(car) {
84
89
  return wheels.length > 0 && wheels.some((wheel) => wheel.inPitLane);
85
90
  }
86
91
 
87
- export function reviewCollisionForSimulation(sim, first, second, collision) {
92
+ function recordPenaltyOnSimulation(penalty, sim) {
93
+ sim.recordPenalty(penalty);
94
+ }
95
+
96
+ export function reviewCollisionForSimulation(sim, first, second, collision, options = {}) {
88
97
  const rule = getPenaltyRule(sim.rules, 'collision');
89
- calculateCollisionPenalties({
98
+ const stats = options.stats ?? sim.runtimeBenchmarkStats ?? null;
99
+ if (stats) stats.collisionStewardReviews = (stats.collisionStewardReviews ?? 0) + 1;
100
+ const context = writeCollisionStewardContext(
101
+ prepareCollisionStewardContext(options.scratch ?? null),
102
+ first,
103
+ second,
104
+ collision,
105
+ options.trackLength ?? null,
106
+ );
107
+ emitCollisionPenalties({
90
108
  first,
91
109
  second,
92
- collision: createCollisionStewardContext(first, second, collision),
110
+ collision: context,
93
111
  rule,
94
- }).forEach((penalty) => sim.recordPenalty(penalty));
112
+ emit: recordPenaltyOnSimulation,
113
+ emitContext: sim,
114
+ });
115
+ if (stats) stats.collisionStewardPenaltyArrayAllocations = stats.collisionStewardPenaltyArrayAllocations ?? 0;
95
116
  }
96
117
 
97
118
  export function reviewTireRequirementForSimulation(sim, car) {
@@ -13,6 +13,7 @@ import {
13
13
  serializeCar,
14
14
  serializeObservationCar,
15
15
  serializeRenderCar,
16
+ serializeRenderCarInto,
16
17
  serializeTrainingCar,
17
18
  } from '../vehicle/vehicleSnapshots.js';
18
19
  import { createLapTelemetry, serializeLapTelemetry } from '../timing/raceTiming.js';
@@ -113,6 +114,33 @@ export function snapshotRaceRender(sim) {
113
114
  };
114
115
  }
115
116
 
117
+ export function snapshotRaceRenderInto(sim, target = {}) {
118
+ const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
119
+ const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
120
+ target.time = sim.time;
121
+ target.world = WORLD;
122
+ target.physicsMode = sim.physicsMode;
123
+ target.track = sim.track;
124
+ target.totalLaps = sim.totalLaps;
125
+ target.raceControl ??= {};
126
+ target.raceControl.mode = sim.raceControl.mode;
127
+ target.raceControl.redFlag = Boolean(sim.raceControl.redFlag);
128
+ target.raceControl.pitLaneOpen = pitLaneStatus.open;
129
+ target.raceControl.pitLaneStatus = pitLaneStatus;
130
+ target.raceControl.finished = sim.raceControl.finished;
131
+ target.raceControl.start = visibleStartState(sim);
132
+ target.pitLaneStatus = pitLaneStatus;
133
+ target.safetyCar = Object.assign(target.safetyCar ?? {}, sim.safetyCar);
134
+ target.cars = renderCarsForSnapshotInto(
135
+ target.cars ?? [],
136
+ sim,
137
+ sim.orderedCars(),
138
+ vehicleSnapshotDependencies,
139
+ );
140
+ target.replayGhosts = serializeReplayGhosts(sim.replayGhosts);
141
+ return target;
142
+ }
143
+
116
144
  export function snapshotRaceObservation(sim) {
117
145
  const pitLaneStatus = pitLaneStatusSnapshot(sim.raceControl, sim.track.pitLane, sim.rules.modules?.pitStops);
118
146
  const vehicleSnapshotDependencies = createVehicleSnapshotDependencies(sim);
@@ -191,3 +219,12 @@ function carsForSnapshot(sim, ordered) {
191
219
  .map((car) => ({ car, rank: affectsRaceOrder(car) ? (car.rank ?? null) : null }));
192
220
  return [...ranked, ...unranked];
193
221
  }
222
+
223
+ function renderCarsForSnapshotInto(targetCars, sim, ordered, dependencies) {
224
+ const entries = carsForSnapshot(sim, ordered);
225
+ targetCars.length = entries.length;
226
+ entries.forEach(({ car }, index) => {
227
+ targetCars[index] = serializeRenderCarInto(targetCars[index] ?? {}, car, dependencies);
228
+ });
229
+ return targetCars;
230
+ }
@@ -25,63 +25,176 @@ function isProgressInZone(track, progress, zone) {
25
25
  : wrapped >= start || wrapped <= end;
26
26
  }
27
27
 
28
+ function getZoneById(zones, zoneId) {
29
+ for (let index = 0; index < zones.length; index += 1) {
30
+ if (zones[index].id === zoneId) return zones[index];
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function findNextDrsZoneIndex(track, progress) {
36
+ const zones = track?.drsZones ?? [];
37
+ if (!zones.length) return -1;
38
+ let bestIndex = -1;
39
+ let bestDistance = Infinity;
40
+ for (let index = 0; index < zones.length; index += 1) {
41
+ const distance = distanceForward(progress, zones[index].start, track.length);
42
+ if (distance <= 1e-6 || distance >= bestDistance) continue;
43
+ bestDistance = distance;
44
+ bestIndex = index;
45
+ }
46
+ return bestIndex >= 0 ? bestIndex : 0;
47
+ }
48
+
49
+ function resolveNextDrsZone(track, car, progress, runtimeBenchmarkStats = null) {
50
+ const zones = track?.drsZones ?? [];
51
+ if (!zones.length) {
52
+ car._drsNextZoneIndex = null;
53
+ return null;
54
+ }
55
+ const cachedIndex = car._drsNextZoneIndex;
56
+ if (Number.isInteger(cachedIndex) && cachedIndex >= 0 && cachedIndex < zones.length) {
57
+ if (runtimeBenchmarkStats) {
58
+ runtimeBenchmarkStats.drsNextZoneCacheHits = (runtimeBenchmarkStats.drsNextZoneCacheHits ?? 0) + 1;
59
+ }
60
+ return zones[cachedIndex];
61
+ }
62
+ const nextIndex = findNextDrsZoneIndex(track, progress);
63
+ car._drsNextZoneIndex = nextIndex;
64
+ if (runtimeBenchmarkStats) {
65
+ runtimeBenchmarkStats.drsNextZoneScans = (runtimeBenchmarkStats.drsNextZoneScans ?? 0) + 1;
66
+ }
67
+ return nextIndex >= 0 ? zones[nextIndex] : null;
68
+ }
69
+
70
+ function tryCachedDrsZoneNoCrossingFastPath(track, car, runtimeBenchmarkStats = null) {
71
+ const zones = track?.drsZones ?? [];
72
+ const cachedIndex = car._drsNextZoneIndex;
73
+ if (!Number.isInteger(cachedIndex) || cachedIndex < 0 || cachedIndex >= zones.length) return false;
74
+
75
+ const previousProgress = car.previousProgress ?? car.progress;
76
+ const travelled = car.progress - previousProgress;
77
+ if (travelled <= 0 || travelled > track.length / 2) return false;
78
+
79
+ const startOffset = zones[cachedIndex].start - previousProgress;
80
+ if (startOffset > 0 && startOffset <= travelled + 0.001) return false;
81
+
82
+ if (runtimeBenchmarkStats) {
83
+ runtimeBenchmarkStats.drsNextZoneCacheHits = (runtimeBenchmarkStats.drsNextZoneCacheHits ?? 0) + 1;
84
+ runtimeBenchmarkStats.drsNextZoneFastPathChecks = (runtimeBenchmarkStats.drsNextZoneFastPathChecks ?? 0) + 1;
85
+ }
86
+ return true;
87
+ }
88
+
28
89
  export function recordDrsDetection(car, zoneId, currentTime) {
29
- const previous = car.drsDetection?.[zoneId] ?? { passage: 0, time: -Infinity };
30
- const next = { passage: previous.passage + 1, time: currentTime };
31
- car.drsDetection = {
32
- ...(car.drsDetection ?? {}),
33
- [zoneId]: next,
34
- };
90
+ const detections = car.drsDetection ?? (car.drsDetection = {});
91
+ const next = detections[zoneId] ?? { passage: 0, time: -Infinity };
92
+ next.passage += 1;
93
+ next.time = currentTime;
94
+ detections[zoneId] = next;
35
95
  return next;
36
96
  }
37
97
 
38
- export function updateDrsLatch(car, ahead, {
39
- hasReference = Boolean(ahead),
40
- safetyCarDeployed = false,
98
+ function updateDrsLatchCore(
99
+ car,
100
+ ahead,
101
+ hasReference,
102
+ safetyCarDeployed,
41
103
  time,
42
104
  track,
43
105
  rules,
44
- } = {}) {
106
+ runtimeBenchmarkStats = null,
107
+ ) {
45
108
  if (safetyCarDeployed || car.finished) {
46
109
  car.drsEligible = false;
47
110
  car.drsActive = false;
48
111
  car.drsZoneId = null;
49
112
  car.drsZoneEnabled = false;
113
+ car._drsZoneRef = null;
50
114
  return;
51
115
  }
52
116
 
53
117
  const previousProgress = car.previousProgress ?? car.progress;
54
- const currentZone = car.drsZoneId
55
- ? track.drsZones.find((zone) => zone.id === car.drsZoneId)
56
- : null;
118
+ const zones = track.drsZones;
119
+ let activeZone = null;
120
+ if (car.drsZoneId) {
121
+ activeZone = car._drsZoneRef?.id === car.drsZoneId ? car._drsZoneRef : null;
122
+ if (!activeZone) {
123
+ activeZone = getZoneById(zones, car.drsZoneId);
124
+ if (activeZone) car._drsZoneRef = activeZone;
125
+ }
126
+ }
57
127
 
58
- if (currentZone && !isProgressInZone(track, car.progress, currentZone)) {
128
+ if (activeZone && !isProgressInZone(track, car.progress, activeZone)) {
59
129
  car.drsZoneId = null;
60
130
  car.drsZoneEnabled = false;
131
+ car._drsZoneRef = null;
132
+ activeZone = null;
61
133
  }
62
134
 
63
135
  if (!car.drsZoneId) {
64
- const crossedZone = track.drsZones.find((zone) => (
65
- crossesDistance(previousProgress, car.progress, zone.start, track.length)
66
- ));
136
+ if (tryCachedDrsZoneNoCrossingFastPath(track, car, runtimeBenchmarkStats)) {
137
+ car.drsEligible = false;
138
+ car.drsActive = false;
139
+ return;
140
+ }
141
+ const crossedZone = resolveNextDrsZone(track, car, previousProgress, runtimeBenchmarkStats);
67
142
  if (crossedZone) {
68
- car.drsZoneId = crossedZone.id;
69
- const crossing = recordDrsDetection(car, crossedZone.id, time);
70
- const aheadCrossing = ahead?.drsDetection?.[crossedZone.id];
71
- car.drsZoneEnabled = Boolean(
72
- hasReference &&
73
- aheadCrossing &&
74
- aheadCrossing.passage === crossing.passage &&
75
- crossing.time >= aheadCrossing.time &&
76
- crossing.time - aheadCrossing.time <= rules.drsDetectionSeconds + 1e-6
77
- );
143
+ const nextZoneIndex = car._drsNextZoneIndex;
144
+ if (crossesDistance(previousProgress, car.progress, crossedZone.start, track.length)) {
145
+ car.drsZoneId = crossedZone.id;
146
+ car._drsZoneRef = crossedZone;
147
+ activeZone = crossedZone;
148
+ if (Number.isInteger(nextZoneIndex) && nextZoneIndex >= 0 && nextZoneIndex < zones.length) {
149
+ car._drsNextZoneIndex = findNextDrsZoneIndex(track, car.progress);
150
+ }
151
+ const crossing = recordDrsDetection(car, crossedZone.id, time);
152
+ const aheadCrossing = ahead?.drsDetection?.[crossedZone.id];
153
+ car.drsZoneEnabled = Boolean(
154
+ hasReference &&
155
+ aheadCrossing &&
156
+ aheadCrossing.passage === crossing.passage &&
157
+ crossing.time >= aheadCrossing.time &&
158
+ crossing.time - aheadCrossing.time <= rules.drsDetectionSeconds + 1e-6
159
+ );
160
+ }
78
161
  }
79
162
  }
80
163
 
81
- const activeZone = car.drsZoneId
82
- ? track.drsZones.find((zone) => zone.id === car.drsZoneId)
83
- : null;
84
164
  const inLatchedZone = activeZone ? isProgressInZone(track, car.progress, activeZone) : false;
85
165
  car.drsEligible = Boolean(car.drsZoneEnabled && inLatchedZone);
86
166
  car.drsActive = car.drsEligible;
87
167
  }
168
+
169
+ export function updateDrsLatch(car, ahead, {
170
+ hasReference = Boolean(ahead),
171
+ safetyCarDeployed = false,
172
+ time,
173
+ track,
174
+ rules,
175
+ runtimeBenchmarkStats = null,
176
+ } = {}) {
177
+ return updateDrsLatchCore(
178
+ car,
179
+ ahead,
180
+ hasReference,
181
+ safetyCarDeployed,
182
+ time,
183
+ track,
184
+ rules,
185
+ runtimeBenchmarkStats,
186
+ );
187
+ }
188
+
189
+ export function updateDrsLatchForSimulation(sim, car, ahead, hasReference = Boolean(ahead)) {
190
+ return updateDrsLatchCore(
191
+ car,
192
+ ahead,
193
+ hasReference,
194
+ sim.safetyCar.deployed,
195
+ sim.time,
196
+ sim.track,
197
+ sim.rules,
198
+ sim.runtimeBenchmarkStats,
199
+ );
200
+ }
@@ -3,8 +3,12 @@ import { getTimingLineCrossingTime, getTimingLineNumber } from './timingLines.js
3
3
 
4
4
  export function estimateGapAheadSeconds(ahead, car, currentTime, track) {
5
5
  if (!ahead) return Infinity;
6
- const timingLineGap = estimateTimingLineGapSeconds(ahead, car, currentTime, track);
7
- if (Number.isFinite(timingLineGap)) return timingLineGap;
6
+ const aheadLastStored = ahead.timingLineLastStored;
7
+ const carLastStored = car.timingLineLastStored;
8
+ if (Number.isInteger(aheadLastStored) && Number.isInteger(carLastStored)) {
9
+ const timingLineGap = estimateTimingLineGapSeconds(ahead, car, currentTime, track);
10
+ if (Number.isFinite(timingLineGap)) return timingLineGap;
11
+ }
8
12
 
9
13
  const crossingTime = interpolateTimeAtDistance(ahead.timingHistory, car.raceDistance);
10
14
  if (Number.isFinite(crossingTime)) {
@@ -22,11 +26,29 @@ export function wholeLapGap(aheadDistance, carDistance, trackLength) {
22
26
 
23
27
  export function estimateTimingLineGapSeconds(ahead, car, currentTime, track) {
24
28
  if (!ahead || !car) return Infinity;
29
+ const aheadLastStored = ahead.timingLineLastStored;
30
+ const carLastStored = car.timingLineLastStored;
31
+ if (!Number.isInteger(aheadLastStored) || !Number.isInteger(carLastStored)) return null;
32
+ const aheadFirstStored = ahead.timingLineFirstStored;
33
+ const carFirstStored = car.timingLineFirstStored;
34
+ if (!Number.isInteger(aheadFirstStored) || !Number.isInteger(carFirstStored)) return null;
25
35
  const currentLine = getTimingLineNumber(track, Math.min(ahead.raceDistance ?? 0, car.raceDistance ?? 0));
26
36
  if (currentLine == null) return null;
27
37
  const oldestLine = Math.max(0, currentLine - (track.timingLines?.count ?? 0));
38
+ const startLine = Math.min(
39
+ currentLine,
40
+ aheadLastStored,
41
+ carLastStored,
42
+ );
43
+ const earliestLine = Math.max(
44
+ oldestLine,
45
+ aheadFirstStored,
46
+ carFirstStored,
47
+ );
48
+
49
+ if (startLine < earliestLine) return null;
28
50
 
29
- for (let lineNumber = currentLine; lineNumber >= oldestLine; lineNumber -= 1) {
51
+ for (let lineNumber = startLine; lineNumber >= earliestLine; lineNumber -= 1) {
30
52
  const aheadTime = getTimingLineCrossingTime(ahead, lineNumber);
31
53
  const carTime = getTimingLineCrossingTime(car, lineNumber);
32
54
  if (Number.isFinite(aheadTime) && Number.isFinite(carTime)) {