feat: add authenticated realtime presence
This commit is contained in:
@@ -78,6 +78,10 @@ import type {
|
||||
AircraftActionSnapshot,
|
||||
AircraftControllerSnapshot,
|
||||
} from "../aircraft/controller.ts";
|
||||
import type { ScenePeers, ScenePeersOptions } from "../realtime/scenePeers.ts";
|
||||
import type { EntityPoseSnapshot } from "../realtime/types.ts";
|
||||
|
||||
export type CityRealtimePeersOptions = Omit<ScenePeersOptions, "project" | "groundAt">;
|
||||
|
||||
export interface SceneOptions {
|
||||
city: City;
|
||||
@@ -99,6 +103,8 @@ export interface SceneOptions {
|
||||
actorAnchor?: { lat: number; lng: number };
|
||||
/** Optional possessed fixed-wing aircraft; the city supplies its projection and terrain. */
|
||||
aircraft?: Omit<SceneAircraftOptions, "project" | "groundAt">;
|
||||
/** Optional remote authoritative entities. Nothing is imported or allocated when absent. */
|
||||
realtimePeers?: CityRealtimePeersOptions;
|
||||
flights?: FlightSource;
|
||||
/**
|
||||
* Element sets to propagate, if this deployment has any.
|
||||
@@ -205,6 +211,11 @@ export interface SceneHandle {
|
||||
aircraftState(): Readonly<AircraftControllerSnapshot> | null;
|
||||
setAircraftActive(active: boolean): void;
|
||||
aircraftActive(): boolean;
|
||||
/** Canonical remote snapshots only; a no-op when realtime peers were not configured. */
|
||||
upsertRemoteSnapshot(snapshot: EntityPoseSnapshot): boolean;
|
||||
removeRemoteEntity(id: string): boolean;
|
||||
clearRemoteEntities(): void;
|
||||
remoteEntityCount(): number;
|
||||
setMarkers(markers: Marker[]): void;
|
||||
/** Take this city off the stage and release everything it built. */
|
||||
dispose(): void;
|
||||
@@ -401,6 +412,23 @@ export async function createScene(
|
||||
})
|
||||
: null;
|
||||
if (sceneAircraft) scene.add(sceneAircraft.root);
|
||||
// Dynamic on purpose: the offline/default entry must not download four peer
|
||||
// prototypes merely because `createScene` can optionally host them.
|
||||
let realtimePeers: ScenePeers | null = null;
|
||||
if (options.realtimePeers) {
|
||||
const { createScenePeers } = await import("../realtime/scenePeers.ts");
|
||||
realtimePeers = createScenePeers({
|
||||
...options.realtimePeers,
|
||||
project: (lat, lng) => world.project(lat, lng),
|
||||
groundAt: (lat, lng) => world.groundAt(lat, lng),
|
||||
geographicSceneUnitsPerMetre:
|
||||
options.realtimePeers.geographicSceneUnitsPerMetre ?? 1 / world.metresPerUnit,
|
||||
geographicVisualScale:
|
||||
options.realtimePeers.geographicVisualScale ??
|
||||
(city.id === "california" ? 0.025 : 1 / world.metresPerUnit),
|
||||
});
|
||||
scene.add(realtimePeers.root);
|
||||
}
|
||||
|
||||
let flightLayer: FlightLayer | null = null;
|
||||
let flightTimer = 0;
|
||||
@@ -521,6 +549,7 @@ export async function createScene(
|
||||
if (actorPlaying && sceneActor) kit.setPose(sceneActor.followPose());
|
||||
sceneAircraft?.tick(dt);
|
||||
if (aircraftPlaying && sceneAircraft) kit.setPose(sceneAircraft.followPose());
|
||||
realtimePeers?.tick(Date.now());
|
||||
roadTraffic?.tick(dt);
|
||||
clouds.tick(dt);
|
||||
if (options.flights && flightLayer) {
|
||||
@@ -572,6 +601,7 @@ export async function createScene(
|
||||
markerLayer.dispose();
|
||||
roadTraffic?.dispose();
|
||||
sceneAircraft?.dispose();
|
||||
realtimePeers?.dispose();
|
||||
kit.dispose();
|
||||
scene.traverse((obj) => {
|
||||
const mesh = obj as THREE.Mesh;
|
||||
@@ -644,12 +674,26 @@ export async function createScene(
|
||||
kit.camera.updateProjectionMatrix();
|
||||
},
|
||||
aircraftActive: () => sceneAircraft?.active() ?? false,
|
||||
upsertRemoteSnapshot: (snapshot) => {
|
||||
if (snapshot.pose.space === "local") {
|
||||
// Local office coordinates have no meaning on a geographic board.
|
||||
// Local city coordinates are accepted only for this detailed board;
|
||||
// California actors use geographic poses.
|
||||
const expected = city.id === "sf" ? "bay-area" : city.id === "socal" ? "socal" : null;
|
||||
if (snapshot.pose.cell.kind !== "city" || snapshot.pose.cell.cityId !== expected) return false;
|
||||
}
|
||||
return realtimePeers?.upsert(snapshot) ?? false;
|
||||
},
|
||||
removeRemoteEntity: (id) => realtimePeers?.remove(id) ?? false,
|
||||
clearRemoteEntities: () => { realtimePeers?.clear(); },
|
||||
remoteEntityCount: () => realtimePeers?.count() ?? 0,
|
||||
setMarkers(markers) {
|
||||
markerLayer.setMarkers(markers);
|
||||
},
|
||||
dispose() {
|
||||
sceneActor?.dispose();
|
||||
sceneAircraft?.dispose();
|
||||
realtimePeers?.dispose();
|
||||
/**
|
||||
* Off the stage, then released — and the stage itself is left running.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user