1
0

feat: complete actor handoff and piloted aircraft presence

This commit is contained in:
2026-08-11 22:17:06 -07:00
parent 655c383061
commit 3326d2e6d0
20 changed files with 907 additions and 41 deletions
+27 -3
View File
@@ -37,6 +37,7 @@ export interface RealtimeServiceOptions {
rateWindowMs?: number;
actorMotionLimits?: MotionLimits;
vehicleMotionLimits?: MotionLimits;
aircraftMotionLimits?: MotionLimits;
}
export interface RealtimeJoinInput {
@@ -103,6 +104,7 @@ interface ResolvedOptions {
rateWindowMs: number;
actorMotionLimits: MotionLimits;
vehicleMotionLimits: MotionLimits;
aircraftMotionLimits: MotionLimits;
}
export interface RealtimeService {
@@ -176,6 +178,13 @@ function resolveOptions(value: RealtimeServiceOptions): ResolvedOptions {
positionSlackM: 4,
maximumDeltaSeconds: 2,
},
aircraftMotionLimits: value.aircraftMotionLimits ?? {
maximumHorizontalSpeedMps: 125,
maximumVerticalSpeedMps: 45,
maximumTurnRateDegPerSec: 180,
positionSlackM: 6,
maximumDeltaSeconds: 2,
},
};
}
@@ -208,7 +217,9 @@ function clonePose(pose: EntityPoseSnapshot): EntityPoseSnapshot {
}
function entityKey(pose: EntityPoseSnapshot): string {
return pose.entity === "actor" ? `actor:${pose.actorId}` : `vehicle:${pose.vehicleId}`;
if (pose.entity === "actor") return `actor:${pose.actorId}`;
if (pose.entity === "vehicle") return `vehicle:${pose.vehicleId}`;
return `aircraft:${pose.aircraftId}`;
}
function poseInterestKey(pose: EntityPoseSnapshot): string | null {
@@ -569,8 +580,16 @@ export function createRealtimeService(options: RealtimeServiceOptions = {}): Rea
}
if (
(snapshot.entity === "actor" && snapshot.actorId !== session.actorId) ||
(snapshot.entity === "vehicle" && snapshot.driverActorId !== session.actorId)
(snapshot.entity === "vehicle" && snapshot.driverActorId !== session.actorId) ||
(snapshot.entity === "aircraft" && snapshot.pilotActorId !== session.actorId)
) return { ok: false, code: "ownership", message: "Realtime entity is not owned by this session." };
if (snapshot.entity === "aircraft" && snapshot.pose.space !== "geographic") {
return {
ok: false,
code: "interest",
message: "Aircraft poses require an outdoor geographic interest.",
};
}
const localKey = poseInterestKey(snapshot);
if (localKey !== null && !session.interestKeys.has(localKey)) {
return { ok: false, code: "interest", message: "Realtime pose is outside the joined interest." };
@@ -585,10 +604,15 @@ export function createRealtimeService(options: RealtimeServiceOptions = {}): Rea
const key = entityKey(snapshot);
const previous = session.proposedPoses.get(key);
if (previous) {
const motionLimits = snapshot.entity === "vehicle"
? settings.vehicleMotionLimits
: snapshot.entity === "aircraft"
? settings.aircraftMotionLimits
: settings.actorMotionLimits;
const motion = validateMotion(
previous,
snapshot,
snapshot.entity === "vehicle" ? settings.vehicleMotionLimits : settings.actorMotionLimits,
motionLimits,
);
if (!motion.ok) {
return { ok: false, code: "motion", message: `Realtime motion rejected: ${motion.reason}.` };
+89
View File
@@ -3,6 +3,7 @@ import { createHmac } from "node:crypto";
import { after, describe, it } from "node:test";
import type {
ActorPoseSnapshot,
AircraftPoseSnapshot,
FloorInterest,
InterestCell,
PoseDelta,
@@ -102,6 +103,37 @@ function vehiclePose(
};
}
function aircraftPose(
pilotActorId: string,
sequence: number,
timestampMs: number,
lat = 37.7,
): AircraftPoseSnapshot {
return {
entity: "aircraft",
aircraftId: `aircraft-${pilotActorId}`,
kind: "electric-vtail",
pilotActorId,
sequence,
timestampMs,
pose: {
space: "geographic",
lat,
lng: -122.4,
altitudeM: 1_500,
headingDeg: 320,
pitchDeg: 2,
},
velocity: { xMps: -40, yMps: 2, zMps: 48, yawDegPerSec: 2 },
rollDeg: -12,
throttle: 0.7,
rollInput: -0.25,
pitchInput: 0.1,
yawInput: 0,
fanRadians: 3,
};
}
function joined(
service: ReturnType<typeof createRealtimeService>,
actorId: string,
@@ -211,6 +243,63 @@ describe("authoritative realtime service", () => {
assert.equal(socalMessages.length, 0);
});
it("converges two clients on an owned aircraft and rejects authority, space, and speed attacks", () => {
let now = 16_000;
const service = createRealtimeService({ now: () => now });
const bay = { kind: "city" as const, cityId: "bay-area" as const };
const pilot = joined(service, "opaque-pilot", [bay], "private-pilot-subject");
const observer = joined(service, "opaque-observer", [bay], "private-observer-subject");
const received: PoseDelta[] = [];
service.connect(observer.sessionId, observer.token, (message) => {
if (message.type === "pose-delta") received.push(message);
});
const first = service.submitPose(
pilot.sessionId,
pilot.token,
aircraftPose("opaque-pilot", 1, now),
);
assert.equal(first.ok, true);
assert.equal(received.length, 1);
assert.equal(received[0]?.updates[0]?.entity, "aircraft");
assert.equal(JSON.stringify(received[0]).includes("private-pilot-subject"), false);
const stolen = service.submitPose(
observer.sessionId,
observer.token,
aircraftPose("opaque-pilot", 1, now),
);
assert.equal(stolen.ok, false);
if (!stolen.ok) assert.equal(stolen.code, "ownership");
const local = aircraftPose("opaque-pilot", 2, now);
const invalidSpace = service.submitPose(pilot.sessionId, pilot.token, {
...local,
pose: { space: "local", cell: bay, xM: 0, yM: 0, zM: 0, headingDeg: 0, pitchDeg: 0 },
});
assert.equal(invalidSpace.ok, false);
if (!invalidSpace.ok) assert.equal(invalidSpace.code, "interest");
now += 1_000;
assert.equal(service.submitPose(
pilot.sessionId,
pilot.token,
aircraftPose("opaque-pilot", 2, now, 37.7005),
).ok, true);
now += 100;
const speedHack = service.submitPose(
pilot.sessionId,
pilot.token,
aircraftPose("opaque-pilot", 3, now, 38.7),
);
assert.equal(speedHack.ok, false);
if (!speedHack.ok) assert.equal(speedHack.code, "motion");
const lateObserver = joined(service, "opaque-late-observer", [bay]);
assert.equal(lateObserver.grant.initial.length, 1);
assert.equal(lateObserver.grant.initial[0]?.entity, "aircraft");
});
it("removes the previous entity when one session switches actor to vehicle", () => {
let now = 17_000;
const service = createRealtimeService({ now: () => now });