Skip to content

Commit 0d74f27

Browse files
authored
feat: migrate fuel renderer to channel snapshots (#652)
1 parent 9d3ca51 commit 0d74f27

24 files changed

Lines changed: 1230 additions & 408 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Decorator } from '@storybook/react-vite';
2+
import { useEffect, useRef } from 'react';
3+
import type {
4+
ChannelBridge,
5+
ChannelName,
6+
ChannelPayloads,
7+
} from '@irdashies/types';
8+
9+
type ChannelSnapshots = Partial<ChannelPayloads>;
10+
11+
export const ChannelSnapshotDecorator = (
12+
snapshots: ChannelSnapshots
13+
): Decorator => {
14+
const bridge: ChannelBridge = {
15+
subscribe: <K extends ChannelName>(
16+
channel: K,
17+
callback: (payload: ChannelPayloads[K]) => void
18+
) => {
19+
const snapshot = snapshots[channel];
20+
if (snapshot !== undefined) callback(snapshot as ChannelPayloads[K]);
21+
return () => undefined;
22+
},
23+
};
24+
25+
const DecoratorComponent: Decorator = (Story) => {
26+
const previousBridge = useRef(window.channelBridge);
27+
window.channelBridge = bridge;
28+
useEffect(
29+
() => () => {
30+
if (previousBridge.current === undefined) {
31+
Reflect.deleteProperty(window, 'channelBridge');
32+
} else {
33+
window.channelBridge = previousBridge.current;
34+
}
35+
},
36+
[]
37+
);
38+
return <Story />;
39+
};
40+
return DecoratorComponent;
41+
};

.storybook/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './telemetryDecorator';
2+
export * from './channelSnapshotDecorator';
23
export * from './DynamicTelemetrySelector';
3-
export * from './mockDashboardBridge';
4+
export * from './mockDashboardBridge';

src/app/bridge/iracingSdk/iracingSdkBridge.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ export async function publishIRacingSDKEvents(
146146
perfMetrics.startReporting();
147147
const fuelProjectionRuntime =
148148
lifecycle && channelBus
149-
? new FuelProjectionRuntime(channelBus, lifecycle, perfMetrics)
149+
? new FuelProjectionRuntime(channelBus, lifecycle, perfMetrics, {
150+
aggregateReplay: isTapeReplay,
151+
})
150152
: undefined;
151153

152154
let shouldStop = false;

src/app/processors/FuelProjectionProcessor.spec.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
22
import type { Session, Telemetry } from '@irdashies/types';
33
import { FuelProjectionProcessor } from './FuelProjectionProcessor';
44

5-
const frame = (values: Record<string, number>): Telemetry =>
5+
const frame = (values: Record<string, number | boolean>): Telemetry =>
66
Object.fromEntries(
77
Object.entries(values).map(([key, entry]) => [key, { value: [entry] }])
88
) as unknown as Telemetry;
@@ -76,6 +76,98 @@ describe('FuelProjectionProcessor', () => {
7676
});
7777
});
7878

79+
it('preserves boolean track and pit state in the snapshot', () => {
80+
const processor = new FuelProjectionProcessor();
81+
82+
processor.onFrame(
83+
frame({ FuelLevel: 20, IsOnTrack: true, OnPitRoad: true })
84+
);
85+
86+
expect(processor.snapshot().isOnTrack).toBe(true);
87+
expect(processor.snapshot().engine.wasOnPitRoad).toBe(true);
88+
});
89+
90+
it('projects timed-race distance using class pace', () => {
91+
const processor = new FuelProjectionProcessor();
92+
processor.init({
93+
DriverInfo: {
94+
DriverCarIdx: 0,
95+
Drivers: [{ CarIdx: 0, CarClassEstLapTime: 90 }],
96+
},
97+
SessionInfo: {
98+
Sessions: [
99+
{ SessionNum: 0, SessionType: 'Race', SessionLaps: 'unlimited' },
100+
],
101+
},
102+
} as unknown as Session);
103+
104+
const timedRaceFrame = {
105+
FuelLevel: { value: [45.39] },
106+
Lap: { value: [2] },
107+
LapDistPct: { value: [0.1] },
108+
SessionNum: { value: [0] },
109+
SessionState: { value: [4] },
110+
SessionLapsRemain: { value: [32767] },
111+
SessionTimeRemain: { value: [1620] },
112+
SessionTimeTotal: { value: [1800] },
113+
CamCarIdx: { value: [0] },
114+
CarIdxLap: { value: [2] },
115+
CarIdxLapDistPct: { value: [0.1] },
116+
CarIdxPosition: { value: [1] },
117+
CarIdxLastLapTime: { value: [1] },
118+
CarIdxBestLapTime: { value: [1] },
119+
} as unknown as Telemetry;
120+
processor.onFrame(timedRaceFrame);
121+
122+
expect(processor.snapshot()).toMatchObject({
123+
calculatedTotalRaceLaps: 19.1,
124+
isFixedLapRace: false,
125+
});
126+
127+
processor.onFrame({
128+
...timedRaceFrame,
129+
CarIdxLastLapTime: { value: [100] },
130+
} as unknown as Telemetry);
131+
expect(processor.snapshot().calculatedTotalRaceLaps).toBeCloseTo(19.1);
132+
133+
processor.onFrame({
134+
...timedRaceFrame,
135+
CarIdxLastLapTime: { value: [110] },
136+
} as unknown as Telemetry);
137+
expect(processor.snapshot().calculatedTotalRaceLaps).toBeCloseTo(19.1);
138+
});
139+
140+
it('uses fractional distance when correcting a timed race for lapping', () => {
141+
const processor = new FuelProjectionProcessor();
142+
processor.init({
143+
DriverInfo: {
144+
DriverCarIdx: 1,
145+
Drivers: [
146+
{ CarIdx: 0, CarClassEstLapTime: 100 },
147+
{ CarIdx: 1, CarClassEstLapTime: 100 },
148+
],
149+
},
150+
SessionInfo: {
151+
Sessions: [
152+
{ SessionNum: 0, SessionType: 'Race', SessionLaps: 'unlimited' },
153+
],
154+
},
155+
} as unknown as Session);
156+
157+
processor.onFrame({
158+
SessionNum: { value: [0] },
159+
SessionState: { value: [4] },
160+
SessionTimeRemain: { value: [900] },
161+
CamCarIdx: { value: [1] },
162+
CarIdxLap: { value: [10, 8] },
163+
CarIdxLapDistPct: { value: [0.1, 0.8] },
164+
CarIdxPosition: { value: [1, 10] },
165+
CarIdxBestLapTime: { value: [100, 100] },
166+
} as unknown as Telemetry);
167+
168+
expect(processor.snapshot().calculatedTotalRaceLaps).toBeCloseTo(17.1);
169+
});
170+
79171
it('resets completed laps when the session number changes', () => {
80172
const processor = new FuelProjectionProcessor();
81173
processor.onFrame(

0 commit comments

Comments
 (0)