feat: add authenticated realtime presence
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
createScenePeers,
|
||||
scenePeerId,
|
||||
type ActorPoseSnapshot,
|
||||
type EntityPoseSnapshot,
|
||||
type InterestCell,
|
||||
type ScenePeersOptions,
|
||||
type VehiclePoseSnapshot,
|
||||
} from "../realtime/index.ts";
|
||||
|
||||
const VELOCITY = { xMps: 0, yMps: 0, zMps: 0, yawDegPerSec: 0 };
|
||||
|
||||
function actor(
|
||||
id: string,
|
||||
kind: "humanoid" | "dog" | "crow",
|
||||
sequence: number,
|
||||
timestampMs: number,
|
||||
xM: number,
|
||||
cell: InterestCell = { kind: "floor", officeId: "hq", floorId: "one" },
|
||||
): ActorPoseSnapshot {
|
||||
if (cell.kind === "california-tile") throw new Error("local poses cannot use tile cells");
|
||||
return {
|
||||
entity: "actor",
|
||||
actorId: id,
|
||||
kind,
|
||||
sequence,
|
||||
timestampMs,
|
||||
pose: { space: "local", cell, xM, yM: 2, zM: 3, headingDeg: 0, pitchDeg: 0 },
|
||||
velocity: { ...VELOCITY, xMps: 2 },
|
||||
};
|
||||
}
|
||||
|
||||
function vehicle(sequence: number, timestampMs: number, lat: number): VehiclePoseSnapshot {
|
||||
return {
|
||||
entity: "vehicle",
|
||||
vehicleId: "ev-1",
|
||||
kind: "model-x",
|
||||
driverActorId: null,
|
||||
sequence,
|
||||
timestampMs,
|
||||
pose: { space: "geographic", lat, lng: -120, altitudeM: 10, headingDeg: 90, pitchDeg: 2 },
|
||||
velocity: { ...VELOCITY, xMps: 20 },
|
||||
steering: 0.4,
|
||||
wheelRadians: 3,
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(over: Partial<ScenePeersOptions> = {}) {
|
||||
return createScenePeers({
|
||||
project: (lat, lng) => [lng * 2, -lat * 3],
|
||||
groundAt: () => 5,
|
||||
geographicSceneUnitsPerMetre: 0.1,
|
||||
localSceneUnitsPerMetre: 1,
|
||||
interpolation: { interpolationDelayMs: 0, maximumExtrapolationMs: 0 },
|
||||
...over,
|
||||
});
|
||||
}
|
||||
|
||||
function mesh(root: THREE.Object3D, name: string): THREE.Mesh {
|
||||
const found = root.getObjectByName(name);
|
||||
if (!(found instanceof THREE.Mesh)) throw new Error(`missing mesh ${name}`);
|
||||
return found;
|
||||
}
|
||||
|
||||
describe("remote scene peers", () => {
|
||||
it("maps local poses directly and keeps one identity-free stable root per peer", () => {
|
||||
const peers = fixture();
|
||||
const first = actor("private-member-id", "humanoid", 1, 1_000, 1);
|
||||
assert.equal(peers.upsert(first), true);
|
||||
assert.deepEqual(peers.ids(), ["actor:private-member-id"]);
|
||||
assert.equal(peers.count(), 1);
|
||||
const root = peers.root.children[0] as THREE.Group;
|
||||
assert.equal(root.name, "remote-peer");
|
||||
assert.equal(JSON.stringify(root.userData).includes("private-member-id"), false);
|
||||
assert.equal(root.getObjectByName("humanoid.face") instanceof THREE.Mesh, true);
|
||||
assert.equal((mesh(root, "humanoid.face").material as THREE.MeshBasicMaterial).map, null);
|
||||
peers.tick(1_000);
|
||||
assert.deepEqual(root.position.toArray(), [1, 2, 3]);
|
||||
|
||||
assert.equal(peers.upsert(actor("private-member-id", "humanoid", 2, 2_000, 5)), true);
|
||||
peers.tick(1_500);
|
||||
assert.equal(peers.root.children[0], root);
|
||||
assert.equal(root.position.x, 3);
|
||||
assert.equal(peers.upsert(actor("private-member-id", "humanoid", 2, 2_100, 8)), false);
|
||||
peers.dispose();
|
||||
});
|
||||
|
||||
it("resets interpolation at local interest-cell boundaries without replacing the root", () => {
|
||||
const peers = fixture();
|
||||
peers.upsert(actor("a", "dog", 1, 1_000, 0));
|
||||
peers.upsert(actor("a", "dog", 2, 2_000, 10));
|
||||
peers.tick(1_500);
|
||||
const root = peers.root.children[0] as THREE.Group;
|
||||
assert.equal(root.position.x, 5);
|
||||
const nextCell = { kind: "floor", officeId: "hq", floorId: "two" } as const;
|
||||
assert.equal(peers.upsert(actor("a", "dog", 3, 3_000, 100, nextCell)), true);
|
||||
peers.tick(2_500);
|
||||
assert.equal(peers.root.children[0], root);
|
||||
assert.equal(root.position.x, 100, "new coordinate frame is held, never mixed with old floor");
|
||||
peers.dispose();
|
||||
});
|
||||
|
||||
it("projects geographic vehicles onto caller terrain and renders a generic black EV", () => {
|
||||
const peers = fixture();
|
||||
const snapshot = vehicle(1, 1_000, 34);
|
||||
assert.equal(scenePeerId(snapshot), "vehicle:ev-1");
|
||||
peers.upsert(snapshot);
|
||||
assert.equal(peers.tick(1_000), 1);
|
||||
const root = peers.root.children[0] as THREE.Group;
|
||||
assert.deepEqual(root.position.toArray(), [-240, 6, -102]);
|
||||
assert.equal(root.scale.x, 0.1);
|
||||
assert.ok(Math.abs(root.rotation.y + Math.PI / 2) < 1e-12);
|
||||
assert.equal(root.getObjectByName("generic-black-ev")?.userData.vehicleModel, "generic-black-ev");
|
||||
assert.equal(root.getObjectByName("frontLeft.spin")?.rotation.x, -snapshot.wheelRadians);
|
||||
const paint = root.getObjectByName("model-x.body:model-x.paint") as THREE.Mesh;
|
||||
assert.equal((paint.material as THREE.MeshStandardMaterial).color.getHex(), 0x050607);
|
||||
peers.dispose();
|
||||
});
|
||||
|
||||
it("shares prototype resources while kind changes retain the entity root", () => {
|
||||
const peers = fixture();
|
||||
peers.upsert(actor("one", "humanoid", 1, 1_000, 0));
|
||||
peers.upsert(actor("two", "humanoid", 1, 1_000, 2));
|
||||
const one = peers.root.children[0] as THREE.Group;
|
||||
const two = peers.root.children[1] as THREE.Group;
|
||||
assert.equal(mesh(one, "humanoid.chest").geometry, mesh(two, "humanoid.chest").geometry);
|
||||
assert.equal(mesh(one, "humanoid.chest").material, mesh(two, "humanoid.chest").material);
|
||||
const oldRig = one.children[0];
|
||||
peers.upsert(actor("one", "crow", 2, 2_000, 1));
|
||||
peers.tick(2_000);
|
||||
assert.equal(peers.root.children[0], one);
|
||||
assert.equal(oldRig?.parent, null);
|
||||
assert.ok(one.getObjectByName("crow"));
|
||||
peers.dispose();
|
||||
});
|
||||
|
||||
it("bounds nearby peers and removes/clears without prematurely disposing shared assets", () => {
|
||||
const peers = fixture({ maximumPeers: 2 });
|
||||
peers.upsert(actor("one", "humanoid", 1, 1_000, 0));
|
||||
peers.upsert(actor("two", "dog", 1, 1_000, 1));
|
||||
assert.equal(peers.upsert(actor("three", "crow", 1, 1_000, 2)), false);
|
||||
const firstPeer = peers.root.children[0];
|
||||
if (!firstPeer) throw new Error("missing first peer");
|
||||
const geometry = mesh(firstPeer, "humanoid.chest").geometry;
|
||||
let disposals = 0;
|
||||
geometry.addEventListener("dispose", () => disposals++);
|
||||
assert.equal(peers.remove("actor:one"), true);
|
||||
assert.equal(peers.remove("actor:one"), false);
|
||||
assert.equal(disposals, 0, "removing one clone cannot release shared prototype resources");
|
||||
peers.clear();
|
||||
assert.equal(peers.count(), 0);
|
||||
assert.deepEqual(peers.ids(), []);
|
||||
assert.equal(disposals, 0);
|
||||
peers.dispose();
|
||||
peers.dispose();
|
||||
assert.equal(disposals, 1);
|
||||
assert.equal(peers.upsert(actor("late", "humanoid", 1, 1_000, 0)), false);
|
||||
});
|
||||
|
||||
it("contains invalid snapshots and projection failures", () => {
|
||||
const peers = fixture({ project: () => [Number.NaN, 0] });
|
||||
assert.equal(peers.upsert({} as EntityPoseSnapshot), false);
|
||||
peers.upsert(vehicle(1, 1_000, 34));
|
||||
assert.equal(peers.tick(1_000), 0);
|
||||
assert.equal(peers.root.children[0]?.visible, false);
|
||||
assert.equal(peers.tick(Number.NaN), 0);
|
||||
peers.dispose();
|
||||
assert.throws(() => fixture({ maximumPeers: 0 }), RangeError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user