Build typed Aetheria client surfaces
This commit is contained in:
parent
74b3b3d72a
commit
2e96660d48
246 changed files with 37893 additions and 6728 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -117,6 +117,20 @@ Aetheria.State.Replica/bin/
|
|||
Aetheria.State.Replica/obj/
|
||||
Aetheria.State.Verify/bin/
|
||||
Aetheria.State.Verify/obj/
|
||||
Aetheria.Rts.Web/bin/
|
||||
Aetheria.Rts.Web/obj/
|
||||
Aetheria.Rts.Web/electron-dist/
|
||||
Aetheria.Rts.Web/logs/
|
||||
Aetheria.Rts.Web/node_modules/
|
||||
Aetheria.Rts.Web/runtime/
|
||||
Aetheria.Rts.Web/Aetheria.Rts.Web/logs/
|
||||
Aetheria.Rts.Web/*.log
|
||||
Aetheria.Rts.Web/wwwroot/*.js
|
||||
Aetheria.State.AuthoritySmoke/bin/
|
||||
Aetheria.State.AuthoritySmoke/obj/
|
||||
Aetheria.State.Freeze/bin/
|
||||
Aetheria.State.Freeze/obj/
|
||||
Aetheria.State.Daemon/*.log
|
||||
|
||||
# JetBrains Rider .idea folder
|
||||
.idea/
|
||||
|
|
|
|||
278
Aetheria.Rts.Web/Client/aetheria-rts-contract.ts
Normal file
278
Aetheria.Rts.Web/Client/aetheria-rts-contract.ts
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
// <auto-generated />
|
||||
// Generated by scripts/generate-rts-bindings.mjs from the RTS CultMesh surface contract.
|
||||
|
||||
import type {
|
||||
CultMeshOperationReceipt,
|
||||
CultMeshSurfaceCatalogDiagnostic,
|
||||
CultMeshSurfaceCatalogIndexDiagnostic,
|
||||
CultMeshViewportRequest,
|
||||
} from "cultmesh-ts";
|
||||
|
||||
export type Viewport = CultMeshViewportRequest;
|
||||
|
||||
export type AetheriaRuntimeSetMoveVectorRequest = {
|
||||
actorEntityKey: string;
|
||||
directionX: number;
|
||||
directionY: number;
|
||||
scalar: number;
|
||||
observedFrameId?: number;
|
||||
};
|
||||
|
||||
export type AetheriaRuntimeSetTargetRequest = {
|
||||
actorEntityKey: string;
|
||||
targetEntityKey: string;
|
||||
observedFrameId?: number;
|
||||
};
|
||||
|
||||
export type AetheriaRuntimeDaemonCommandReceipt = CultMeshOperationReceipt & {
|
||||
commandId: string;
|
||||
};
|
||||
|
||||
export type AetheriaRuntimeSelectedObjectRequest = {
|
||||
entityIndex: number;
|
||||
};
|
||||
|
||||
export type AetheriaRuntimeViewportFeedRequest = {
|
||||
viewport: Viewport;
|
||||
selectedEntityIndex?: number;
|
||||
};
|
||||
|
||||
export type AetheriaRuntimeViewportFeedSnapshot = {
|
||||
viewport: ViewportResponse;
|
||||
selectedObject: SelectedObjectProjection | null;
|
||||
inventory: InventoryProjection | null;
|
||||
daemonHealth: DaemonHealthProjection;
|
||||
authorityStatus: AuthorityStatusProjection;
|
||||
starbridgeSession: StarbridgeSessionProjection;
|
||||
receivedAtUtc: string;
|
||||
sampleMs: number;
|
||||
};
|
||||
|
||||
export type ViewportResponse = {
|
||||
schema: string;
|
||||
frameId: number;
|
||||
publishedAtUtc: string;
|
||||
simulationTimeSeconds: number;
|
||||
runId: string;
|
||||
zoneIndex: number;
|
||||
zoneName: string;
|
||||
currentEntityKey: string;
|
||||
viewport: Viewport;
|
||||
controlledEntityIndices: number[];
|
||||
objects: ViewObject[];
|
||||
gravityInfluences: GravityInfluence[];
|
||||
bodies: BodyView[];
|
||||
};
|
||||
|
||||
export type ObjectsViewportResponse = {
|
||||
schema: string;
|
||||
frameId: number;
|
||||
publishedAtUtc: string;
|
||||
simulationTimeSeconds: number;
|
||||
runId: string;
|
||||
zoneIndex: number;
|
||||
zoneName: string;
|
||||
currentEntityKey: string;
|
||||
viewport: Viewport;
|
||||
controlledEntityIndices: number[];
|
||||
objects: ViewObject[];
|
||||
};
|
||||
|
||||
export type GravityViewportResponse = {
|
||||
schema: string;
|
||||
frameId: number;
|
||||
publishedAtUtc: string;
|
||||
simulationTimeSeconds: number;
|
||||
runId: string;
|
||||
zoneIndex: number;
|
||||
zoneName: string;
|
||||
viewport: Viewport;
|
||||
gravityInfluences: GravityInfluence[];
|
||||
bodies: BodyView[];
|
||||
};
|
||||
|
||||
export type ViewObject = {
|
||||
entityIndex: number;
|
||||
entityKey: string;
|
||||
displayName: string;
|
||||
kind: string;
|
||||
factionKey: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
directionX: number;
|
||||
directionY: number;
|
||||
velocityX: number;
|
||||
velocityY: number;
|
||||
controlled: boolean;
|
||||
targetEntityIndex: number;
|
||||
isActive: boolean;
|
||||
visibility: number;
|
||||
status: EntityStatus;
|
||||
inventory: InventoryItem[];
|
||||
};
|
||||
|
||||
export type EntityStatus = {
|
||||
hull: number;
|
||||
shield: number;
|
||||
heat: number;
|
||||
};
|
||||
|
||||
export type InventoryItem = {
|
||||
source: string;
|
||||
itemKey: string;
|
||||
quantity: number;
|
||||
quality: number;
|
||||
durability: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type SelectedObjectProjection = {
|
||||
schema: string;
|
||||
frameId: number;
|
||||
runId: string;
|
||||
zoneIndex: number;
|
||||
entityIndex: number;
|
||||
selected: ViewObject | null;
|
||||
};
|
||||
|
||||
export type InventoryProjection = {
|
||||
schema: string;
|
||||
frameId: number;
|
||||
runId: string;
|
||||
zoneIndex: number;
|
||||
entityIndex: number;
|
||||
entityKey: string;
|
||||
items: InventoryItem[];
|
||||
equipment: InventoryItem[];
|
||||
cargo: InventoryItem[];
|
||||
};
|
||||
|
||||
export type DaemonHealthProjection = {
|
||||
schema: string;
|
||||
daemonId: string;
|
||||
verseId: string;
|
||||
publishedAtUtc: string;
|
||||
statePath: string;
|
||||
frameId: number;
|
||||
observedCommandCount: number;
|
||||
appliedCommandCount: number;
|
||||
rejectedCommandCount: number;
|
||||
status: string;
|
||||
publicationSource: string;
|
||||
transport: string;
|
||||
commandBoundaryPath: string;
|
||||
};
|
||||
|
||||
export type AuthorityStatusProjection = {
|
||||
schema: string;
|
||||
verseId: string;
|
||||
policyId: string;
|
||||
ruleVersion: string;
|
||||
hostRuntimeId: string;
|
||||
defaultMode: string;
|
||||
updatedAtUtc: string;
|
||||
rules: AuthorityRuleProjection[];
|
||||
};
|
||||
|
||||
export type AuthorityRuleProjection = {
|
||||
ruleId: string;
|
||||
subjectPrefix: string;
|
||||
claimKinds: string[];
|
||||
mode: string;
|
||||
runtimeIds: string[];
|
||||
leaseScope: string;
|
||||
priority: number;
|
||||
};
|
||||
|
||||
export type StarbridgeSessionProjection = {
|
||||
schema: string;
|
||||
frameId: number;
|
||||
publishedAtUtc: string;
|
||||
sessionId: string;
|
||||
scenarioId: string;
|
||||
scenarioName: string;
|
||||
runId: string;
|
||||
zoneIndex: number;
|
||||
zoneName: string;
|
||||
phase: string;
|
||||
currentWaveIndex: number;
|
||||
baseStatus: StarbridgeBaseStatusProjection;
|
||||
stationStock: StarbridgeStationStockItemProjection[];
|
||||
waveForecast: StarbridgeWaveForecastProjection[];
|
||||
runtimeRoles: StarbridgeRuntimeRoleProjection[];
|
||||
};
|
||||
|
||||
export type StarbridgeBaseStatusProjection = {
|
||||
entityKey: string;
|
||||
displayName: string;
|
||||
hull: number;
|
||||
shield: number;
|
||||
heat: number;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export type StarbridgeStationStockItemProjection = {
|
||||
itemKey: string;
|
||||
quantity: number;
|
||||
quality: number;
|
||||
durability: number;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type StarbridgeWaveForecastProjection = {
|
||||
waveIndex: number;
|
||||
displayName: string;
|
||||
attackerKeys: string[];
|
||||
bossKey: string;
|
||||
recoveredTechnologyKeys: string[];
|
||||
};
|
||||
|
||||
export type StarbridgeRuntimeRoleProjection = {
|
||||
runtimeId: string;
|
||||
role: string;
|
||||
entityKey: string;
|
||||
};
|
||||
|
||||
export type GravityInfluence = {
|
||||
bodyKey: string;
|
||||
orbitKey: string;
|
||||
kind: string;
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
gravityDepth: number;
|
||||
gravityDepthExponent: number;
|
||||
waveRadius: number;
|
||||
waveDepth: number;
|
||||
waveSpeed: number;
|
||||
};
|
||||
|
||||
export type BodyView = {
|
||||
bodyKey: string;
|
||||
orbitKey: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
isAsteroidBelt: boolean;
|
||||
};
|
||||
|
||||
|
||||
export type AetheriaRtsApi = {
|
||||
mapViewport(request: Viewport): Promise<ViewportResponse>;
|
||||
objectsViewport(request: Viewport): Promise<ObjectsViewportResponse>;
|
||||
gravityViewport(request: Viewport): Promise<GravityViewportResponse>;
|
||||
selectedObject(request: AetheriaRuntimeSelectedObjectRequest): Promise<SelectedObjectProjection>;
|
||||
inventory(request: AetheriaRuntimeSelectedObjectRequest): Promise<InventoryProjection>;
|
||||
daemonHealth(): Promise<DaemonHealthProjection>;
|
||||
authorityStatus(): Promise<AuthorityStatusProjection>;
|
||||
starbridgeSession(): Promise<StarbridgeSessionProjection>;
|
||||
watchViewportFeed(request: AetheriaRuntimeViewportFeedRequest, callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void): () => void;
|
||||
setMoveVector(request: AetheriaRuntimeSetMoveVectorRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
|
||||
setTarget(request: AetheriaRuntimeSetTargetRequest): Promise<AetheriaRuntimeDaemonCommandReceipt>;
|
||||
surfaceCatalog(): Promise<CultMeshSurfaceCatalogDiagnostic>;
|
||||
surfaceCatalogIndex(): Promise<CultMeshSurfaceCatalogIndexDiagnostic>;
|
||||
health(): Promise<unknown>;
|
||||
};
|
||||
590
Aetheria.Rts.Web/Client/app.ts
Normal file
590
Aetheria.Rts.Web/Client/app.ts
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
import type {
|
||||
AetheriaRtsApi,
|
||||
AuthorityStatusProjection,
|
||||
AetheriaRuntimeViewportFeedSnapshot,
|
||||
BodyView,
|
||||
DaemonHealthProjection,
|
||||
GravityInfluence,
|
||||
InventoryItem,
|
||||
InventoryProjection,
|
||||
SelectedObjectProjection,
|
||||
StarbridgeSessionProjection,
|
||||
ViewObject,
|
||||
Viewport,
|
||||
ViewportResponse,
|
||||
} from "./aetheria-rts-contract.js";
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
aetheriaRts: AetheriaRtsApi;
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = requiredElement<HTMLCanvasElement>("#map");
|
||||
const statusEl = requiredElement<HTMLElement>("#status");
|
||||
const zoomLabel = requiredElement<HTMLOutputElement>("#zoom-label");
|
||||
const selectionTitle = requiredElement<HTMLElement>("#selection-title");
|
||||
const selectionDetails = requiredElement<HTMLElement>("#selection-details");
|
||||
const frameDetails = requiredElement<HTMLElement>("#frame-details");
|
||||
const starbridgeDetails = requiredElement<HTMLElement>("#starbridge-details");
|
||||
const runtimeSurfaceDetails = requiredElement<HTMLElement>("#runtime-surface-details");
|
||||
const controlledList = requiredElement<HTMLElement>("#controlled-list");
|
||||
const ctx = requiredContext(canvas);
|
||||
|
||||
let zoom = 1;
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
let selectedEntityIndex = -1;
|
||||
let latest: ViewportResponse | null = null;
|
||||
let latestSelected: SelectedObjectProjection | null = null;
|
||||
let latestInventory: InventoryProjection | null = null;
|
||||
let latestDaemonHealth: DaemonHealthProjection | null = null;
|
||||
let latestAuthority: AuthorityStatusProjection | null = null;
|
||||
let latestStarbridge: StarbridgeSessionProjection | null = null;
|
||||
let latestSurfaceIndex: Awaited<ReturnType<AetheriaRtsApi["surfaceCatalogIndex"]>> | null = null;
|
||||
let latestOperationReceipt: string | null = null;
|
||||
let latestReceivedAt = 0;
|
||||
let latestPollMs = 0;
|
||||
let refreshInFlight = false;
|
||||
let viewportFeedUnsubscribe: (() => void) | null = null;
|
||||
|
||||
function requiredElement<TElement extends Element>(selector: string): TElement {
|
||||
const element = document.querySelector<TElement>(selector);
|
||||
if (!element) {
|
||||
throw new Error(`Aetheria RTS shell is missing ${selector}.`);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function requiredContext(target: HTMLCanvasElement): CanvasRenderingContext2D {
|
||||
const context = target.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("Canvas 2D context is unavailable.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const worldSpan = () => 2200 / zoom;
|
||||
|
||||
function currentViewport(): Viewport {
|
||||
const span = worldSpan();
|
||||
const aspect = Math.max(0.25, canvas.clientWidth / Math.max(1, canvas.clientHeight));
|
||||
const width = span * aspect;
|
||||
const height = span;
|
||||
return {
|
||||
minX: centerX - width * 0.5,
|
||||
minY: centerY - height * 0.5,
|
||||
maxX: centerX + width * 0.5,
|
||||
maxY: centerY + height * 0.5
|
||||
};
|
||||
}
|
||||
|
||||
function setStatus(text: string): void {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function startViewportFeed(): void {
|
||||
const viewport = currentViewport();
|
||||
viewportFeedUnsubscribe?.();
|
||||
viewportFeedUnsubscribe = window.aetheriaRts.watchViewportFeed({
|
||||
viewport,
|
||||
selectedEntityIndex
|
||||
}, applyViewportSnapshot);
|
||||
}
|
||||
|
||||
function applyViewportSnapshot(snapshot: AetheriaRuntimeViewportFeedSnapshot): void {
|
||||
latestPollMs = snapshot.sampleMs;
|
||||
latestReceivedAt = Date.parse(snapshot.receivedAtUtc) || Date.now();
|
||||
latest = snapshot.viewport;
|
||||
latestSelected = snapshot.selectedObject;
|
||||
latestInventory = snapshot.inventory;
|
||||
latestDaemonHealth = snapshot.daemonHealth;
|
||||
latestAuthority = snapshot.authorityStatus;
|
||||
latestStarbridge = snapshot.starbridgeSession;
|
||||
|
||||
const controlled = snapshot.viewport.objects.find(object => object.controlled);
|
||||
if (controlled && selectedEntityIndex < 0) {
|
||||
selectedEntityIndex = controlled.entityIndex;
|
||||
centerX = controlled.x;
|
||||
centerY = controlled.y;
|
||||
startViewportFeed();
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(`${snapshot.viewport.zoneName || `Zone ${snapshot.viewport.zoneIndex}`} frame ${snapshot.viewport.frameId}`);
|
||||
render();
|
||||
renderPanels();
|
||||
}
|
||||
|
||||
async function refreshRuntimeSurfaces(): Promise<void> {
|
||||
try {
|
||||
latestSurfaceIndex = await window.aetheriaRts.surfaceCatalogIndex();
|
||||
renderPanels();
|
||||
} catch (error) {
|
||||
runtimeSurfaceDetails.innerHTML = details([
|
||||
["State", error instanceof Error ? error.message : "unavailable"]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeCanvas(): void {
|
||||
const ratio = Math.max(1, window.devicePixelRatio || 1);
|
||||
const width = Math.max(1, Math.floor(canvas.clientWidth * ratio));
|
||||
const height = Math.max(1, Math.floor(canvas.clientHeight * ratio));
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
||||
}
|
||||
|
||||
function worldToScreen(x: number, y: number): { x: number; y: number } {
|
||||
const viewport = currentViewport();
|
||||
const sx = (x - viewport.minX) / (viewport.maxX - viewport.minX) * canvas.clientWidth;
|
||||
const sy = canvas.clientHeight - (y - viewport.minY) / (viewport.maxY - viewport.minY) * canvas.clientHeight;
|
||||
return { x: sx, y: sy };
|
||||
}
|
||||
|
||||
function screenToWorld(x: number, y: number): { x: number; y: number } {
|
||||
const viewport = currentViewport();
|
||||
return {
|
||||
x: viewport.minX + x / Math.max(1, canvas.clientWidth) * (viewport.maxX - viewport.minX),
|
||||
y: viewport.minY + (1 - y / Math.max(1, canvas.clientHeight)) * (viewport.maxY - viewport.minY)
|
||||
};
|
||||
}
|
||||
|
||||
function worldRadiusToScreen(radius: number): number {
|
||||
const viewport = currentViewport();
|
||||
return radius / (viewport.maxX - viewport.minX) * canvas.clientWidth;
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
resizeCanvas();
|
||||
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
|
||||
drawGrid();
|
||||
if (!latest) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const influence of latest.gravityInfluences) {
|
||||
drawGravity(influence);
|
||||
}
|
||||
for (const body of latest.bodies) {
|
||||
drawBody(body);
|
||||
}
|
||||
for (const object of latest.objects) {
|
||||
drawObject(object);
|
||||
}
|
||||
}
|
||||
|
||||
function drawGrid(): void {
|
||||
const viewport = currentViewport();
|
||||
const spacing = chooseGridSpacing((viewport.maxX - viewport.minX) / 8);
|
||||
const startX = Math.floor(viewport.minX / spacing) * spacing;
|
||||
const startY = Math.floor(viewport.minY / spacing) * spacing;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = "#1b242a";
|
||||
ctx.beginPath();
|
||||
for (let x = startX; x <= viewport.maxX; x += spacing) {
|
||||
const screen = worldToScreen(x, viewport.minY);
|
||||
ctx.moveTo(screen.x, 0);
|
||||
ctx.lineTo(screen.x, canvas.clientHeight);
|
||||
}
|
||||
for (let y = startY; y <= viewport.maxY; y += spacing) {
|
||||
const screen = worldToScreen(viewport.minX, y);
|
||||
ctx.moveTo(0, screen.y);
|
||||
ctx.lineTo(canvas.clientWidth, screen.y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function chooseGridSpacing(target: number): number {
|
||||
const power = 10 ** Math.floor(Math.log10(Math.max(1, target)));
|
||||
const scaled = target / power;
|
||||
if (scaled > 5) return power * 10;
|
||||
if (scaled > 2) return power * 5;
|
||||
return power * 2;
|
||||
}
|
||||
|
||||
function drawGravity(influence: GravityInfluence): void {
|
||||
const screen = worldToScreen(influence.x, influence.y);
|
||||
const radius = worldRadiusToScreen(influence.radius);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = influence.kind === "Sun" ? "rgba(246, 192, 82, 0.28)" : "rgba(97, 174, 197, 0.24)";
|
||||
ctx.fillStyle = influence.kind === "Sun" ? "rgba(246, 192, 82, 0.05)" : "rgba(97, 174, 197, 0.04)";
|
||||
ctx.beginPath();
|
||||
ctx.arc(screen.x, screen.y, Math.max(1, radius), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawBody(body: BodyView): void {
|
||||
const screen = worldToScreen(body.x, body.y);
|
||||
const radius = Math.max(3, Math.min(28, worldRadiusToScreen(body.radius) * 0.025));
|
||||
ctx.fillStyle = body.kind === "sun" ? "#f4bc5d" : body.isAsteroidBelt ? "#9d9080" : "#71a8b7";
|
||||
ctx.beginPath();
|
||||
ctx.arc(screen.x, screen.y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawObject(object: ViewObject): void {
|
||||
const screen = worldToScreen(object.x, object.y);
|
||||
const selected = object.entityIndex === selectedEntityIndex;
|
||||
const station = object.kind.toLowerCase().includes("station");
|
||||
const size = selected ? 9 : object.controlled ? 7 : 5;
|
||||
ctx.save();
|
||||
ctx.translate(screen.x, screen.y);
|
||||
ctx.rotate(Math.atan2(object.directionY, object.directionX));
|
||||
ctx.fillStyle = object.isActive
|
||||
? selected ? "#ffffff" : object.controlled ? "#54d18a" : object.factionKey === "raider" ? "#e36b4b" : "#aeb7bd"
|
||||
: "#56616a";
|
||||
ctx.strokeStyle = "#0d1013";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
if (station) {
|
||||
ctx.rect(-size, -size, size * 2, size * 2);
|
||||
} else {
|
||||
ctx.moveTo(size + 4, 0);
|
||||
ctx.lineTo(-size, size * 0.7);
|
||||
ctx.lineTo(-size * 0.45, 0);
|
||||
ctx.lineTo(-size, -size * 0.7);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
if (object.targetEntityIndex >= 0) {
|
||||
const target = latest?.objects.find(candidate => candidate.entityIndex === object.targetEntityIndex);
|
||||
if (target) {
|
||||
const targetScreen = worldToScreen(target.x, target.y);
|
||||
ctx.strokeStyle = object.controlled ? "rgba(84, 209, 138, 0.42)" : "rgba(227, 107, 75, 0.36)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(screen.x, screen.y);
|
||||
ctx.lineTo(targetScreen.x, targetScreen.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
ctx.strokeStyle = "#ffffff";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(screen.x, screen.y, 14, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPanels(): void {
|
||||
if (!latest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = latestSelected?.selected ?? null;
|
||||
selectionTitle.textContent = selected?.displayName || "No Selection";
|
||||
selectionDetails.innerHTML = selected
|
||||
? details([
|
||||
["Entity", selected.entityIndex.toString()],
|
||||
["Key", selected.entityKey],
|
||||
["Kind", selected.kind || "unknown"],
|
||||
["Faction", selected.factionKey || "unknown"],
|
||||
["State", selected.isActive ? "active" : "disabled"],
|
||||
["Hull", selected.status.hull.toFixed(1)],
|
||||
["Shield", selected.status.shield.toFixed(1)],
|
||||
["Heat", selected.status.heat.toFixed(1)],
|
||||
["Position", `${selected.x.toFixed(1)}, ${selected.y.toFixed(1)}, z ${selected.z.toFixed(1)}`],
|
||||
["Velocity", `${selected.velocityX.toFixed(2)}, ${selected.velocityY.toFixed(2)}`],
|
||||
["Target", selected.targetEntityIndex >= 0 ? `#${selected.targetEntityIndex}` : "none"],
|
||||
["Controlled", selected.controlled ? "yes" : "no"],
|
||||
["Equipment", inventoryText(latestInventory?.equipment ?? [])],
|
||||
["Cargo", inventoryText(latestInventory?.cargo ?? [])]
|
||||
])
|
||||
: `<dt class="muted">State</dt><dd class="muted">Click a visible object on the map.</dd>`;
|
||||
|
||||
frameDetails.innerHTML = details([
|
||||
["Run", latest.runId],
|
||||
["Zone", `${latest.zoneIndex}`],
|
||||
["Frame", latest.frameId.toString()],
|
||||
["Sim Time", `${latest.simulationTimeSeconds.toFixed(2)}s`],
|
||||
["Frame Age", `${Math.max(0, Date.now() - latestReceivedAt)}ms`],
|
||||
["Viewport Poll", `${latestPollMs.toFixed(1)}ms`],
|
||||
["Visible", latest.objects.length.toString()],
|
||||
["Daemon", latestDaemonHealth ? `${latestDaemonHealth.status} / ${latestDaemonHealth.transport}` : "unknown"],
|
||||
["Authority", latestAuthority ? latestAuthority.policyId : "unknown"],
|
||||
["Mode", latestAuthority ? latestAuthority.defaultMode : "unknown"],
|
||||
["Last Op", latestOperationReceipt ?? "none"]
|
||||
]);
|
||||
|
||||
runtimeSurfaceDetails.innerHTML = latestSurfaceIndex
|
||||
? details([
|
||||
["Catalog", latestSurfaceIndex.catalogId],
|
||||
["Queries", surfaceListText(latestSurfaceIndex.queries)],
|
||||
["Live Feeds", surfaceListText(latestSurfaceIndex.liveFeeds)],
|
||||
["Operations", surfaceListText(latestSurfaceIndex.operations)],
|
||||
["Pointers", surfaceListText(latestSurfaceIndex.statePointers)],
|
||||
["Native Views", surfaceListText(latestSurfaceIndex.nativeSliceViews)]
|
||||
])
|
||||
: `<dt class="muted">State</dt><dd class="muted">Discovering runtime surfaces.</dd>`;
|
||||
|
||||
starbridgeDetails.innerHTML = latestStarbridge
|
||||
? details([
|
||||
["Scenario", latestStarbridge.scenarioName || latestStarbridge.scenarioId],
|
||||
["Phase", latestStarbridge.phase],
|
||||
["Wave", latestStarbridge.currentWaveIndex.toString()],
|
||||
["Base", latestStarbridge.baseStatus.displayName || "unknown"],
|
||||
["Hull", latestStarbridge.baseStatus.hull.toFixed(1)],
|
||||
["Shield", latestStarbridge.baseStatus.shield.toFixed(1)],
|
||||
["Heat", latestStarbridge.baseStatus.heat.toFixed(1)],
|
||||
["Next", waveText(latestStarbridge)],
|
||||
["Stock", stockText(latestStarbridge)],
|
||||
["Roles", roleText(latestStarbridge)]
|
||||
])
|
||||
: `<dt class="muted">State</dt><dd class="muted">No Starbridge session.</dd>`;
|
||||
|
||||
controlledList.innerHTML = "";
|
||||
const controlledObjects = latest.objects.filter(object => object.controlled);
|
||||
if (controlledObjects.length === 0) {
|
||||
controlledList.innerHTML = `<span class="muted">No controlled units in viewport.</span>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const object of controlledObjects) {
|
||||
const row = document.createElement("button");
|
||||
row.type = "button";
|
||||
row.className = "controlled-row";
|
||||
row.innerHTML = `<span>${escapeHtml(object.displayName || object.entityKey)}</span><span>#${object.entityIndex}</span>`;
|
||||
row.addEventListener("click", () => {
|
||||
selectedEntityIndex = object.entityIndex;
|
||||
centerX = object.x;
|
||||
centerY = object.y;
|
||||
void refresh();
|
||||
});
|
||||
controlledList.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
function inventoryText(items: InventoryItem[]): string {
|
||||
if (items.length === 0) {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return items
|
||||
.map(item => `${item.itemKey} x${item.quantity}`)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function details(rows: Array<[string, string]>): string {
|
||||
return rows
|
||||
.map(([key, value]) => `<dt>${escapeHtml(key)}</dt><dd>${escapeHtml(value)}</dd>`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value.replace(/[&<>"']/g, character => {
|
||||
switch (character) {
|
||||
case "&": return "&";
|
||||
case "<": return "<";
|
||||
case ">": return ">";
|
||||
case "\"": return """;
|
||||
case "'": return "'";
|
||||
default: return character;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canvas.addEventListener("click", event => {
|
||||
if (!latest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const click = screenToWorld(event.clientX - rect.left, event.clientY - rect.top);
|
||||
const nearest = nearestObject(click.x, click.y);
|
||||
|
||||
if (nearest && worldRadiusToScreen(nearest.distance) < 24) {
|
||||
selectedEntityIndex = nearest.object.entityIndex;
|
||||
void refresh();
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("contextmenu", event => {
|
||||
event.preventDefault();
|
||||
if (!latest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = latest.objects.find(object => object.entityIndex === selectedEntityIndex && object.controlled);
|
||||
if (!actor) {
|
||||
setStatus("Select an owned pawn or station first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const click = screenToWorld(event.clientX - rect.left, event.clientY - rect.top);
|
||||
const nearest = nearestObject(click.x, click.y);
|
||||
if (nearest && nearest.object.entityIndex !== actor.entityIndex && worldRadiusToScreen(nearest.distance) < 28) {
|
||||
void issueTargetCommand(actor, nearest.object);
|
||||
return;
|
||||
}
|
||||
|
||||
void issueMoveCommand(actor, click.x, click.y);
|
||||
});
|
||||
|
||||
function nearestObject(x: number, y: number): { object: ViewObject; distance: number } | null {
|
||||
if (!latest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nearest: ViewObject | null = null;
|
||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||
for (const object of latest.objects) {
|
||||
const dx = object.x - x;
|
||||
const dy = object.y - y;
|
||||
const distance = Math.hypot(dx, dy);
|
||||
if (distance < nearestDistance) {
|
||||
nearest = object;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest ? { object: nearest, distance: nearestDistance } : null;
|
||||
}
|
||||
|
||||
async function issueTargetCommand(actor: ViewObject, target: ViewObject): Promise<void> {
|
||||
const receipt = await window.aetheriaRts.setTarget({
|
||||
actorEntityKey: actor.entityKey,
|
||||
targetEntityKey: target.entityKey,
|
||||
observedFrameId: latest?.frameId
|
||||
});
|
||||
latestOperationReceipt = formatOperationReceipt("setTarget", receipt, `${actor.entityKey} -> ${target.entityKey}`);
|
||||
setStatus(latestOperationReceipt);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function issueMoveCommand(actor: ViewObject, x: number, y: number): Promise<void> {
|
||||
const dx = x - actor.x;
|
||||
const dy = y - actor.y;
|
||||
const length = Math.max(0.001, Math.hypot(dx, dy));
|
||||
const receipt = await window.aetheriaRts.setMoveVector({
|
||||
actorEntityKey: actor.entityKey,
|
||||
directionX: dx / length,
|
||||
directionY: dy / length,
|
||||
scalar: 1,
|
||||
observedFrameId: latest?.frameId
|
||||
});
|
||||
latestOperationReceipt = formatOperationReceipt("setMoveVector", receipt, `${actor.entityKey} ${dx.toFixed(1)},${dy.toFixed(1)}`);
|
||||
setStatus(latestOperationReceipt);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
function formatOperationReceipt(
|
||||
label: string,
|
||||
receipt: Awaited<ReturnType<AetheriaRtsApi["setMoveVector"]>>,
|
||||
detail: string
|
||||
): string {
|
||||
const accepted = receipt.accepted ? "accepted" : "rejected";
|
||||
return `${label} ${receipt.operationId} ${accepted} via ${receipt.route.kind} ${receipt.commandId} ${detail} @ frame ${latest?.frameId ?? "?"}`;
|
||||
}
|
||||
|
||||
document.querySelector<HTMLButtonElement>("#zoom-in")?.addEventListener("click", () => {
|
||||
zoom = Math.min(12, zoom * 1.4);
|
||||
updateZoomLabel();
|
||||
void refresh();
|
||||
});
|
||||
|
||||
document.querySelector<HTMLButtonElement>("#zoom-out")?.addEventListener("click", () => {
|
||||
zoom = Math.max(0.2, zoom / 1.4);
|
||||
updateZoomLabel();
|
||||
void refresh();
|
||||
});
|
||||
|
||||
document.querySelector<HTMLButtonElement>("#refresh")?.addEventListener("click", () => {
|
||||
void refresh();
|
||||
});
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
render();
|
||||
});
|
||||
|
||||
function updateZoomLabel(): void {
|
||||
zoomLabel.value = `${zoom.toFixed(1)}x`;
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
if (refreshInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshInFlight = true;
|
||||
try {
|
||||
startViewportFeed();
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : "Unable to fetch viewport");
|
||||
} finally {
|
||||
refreshInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
function surfaceListText(surfaces: ReadonlyArray<{
|
||||
readonly surfaceId: string;
|
||||
readonly routeHint: { readonly kind: string; readonly description?: string };
|
||||
readonly sources: ReadonlyArray<{ readonly sourceId: string; readonly schemaId?: string }>;
|
||||
}>): string {
|
||||
if (surfaces.length === 0) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
return surfaces
|
||||
.map(surface => {
|
||||
const route = surface.routeHint.description
|
||||
? `${surface.routeHint.kind} ${surface.routeHint.description}`
|
||||
: surface.routeHint.kind;
|
||||
const sources = surface.sources.length === 0
|
||||
? "no sources"
|
||||
: surface.sources
|
||||
.map(source => source.schemaId ? `${source.sourceId} (${source.schemaId})` : source.sourceId)
|
||||
.join(" + ");
|
||||
return `${surface.surfaceId} -> ${route}; ${sources}`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function stockText(session: StarbridgeSessionProjection): string {
|
||||
if (session.stationStock.length === 0) {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return session.stationStock
|
||||
.slice(0, 4)
|
||||
.map(item => `${item.itemKey} x${item.quantity}`)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function waveText(session: StarbridgeSessionProjection): string {
|
||||
const wave = session.waveForecast[0];
|
||||
if (!wave) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
const attackers = wave.attackerKeys.length > 0 ? ` / ${wave.attackerKeys.join(", ")}` : "";
|
||||
return `${wave.displayName || `Wave ${wave.waveIndex}`}${attackers}`;
|
||||
}
|
||||
|
||||
function roleText(session: StarbridgeSessionProjection): string {
|
||||
if (session.runtimeRoles.length === 0) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
return session.runtimeRoles
|
||||
.map(role => `${role.role}:${role.runtimeId}`)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
updateZoomLabel();
|
||||
void refreshRuntimeSurfaces();
|
||||
void refresh();
|
||||
443
Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts
Normal file
443
Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
import { encode } from "@msgpack/msgpack";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { CultMesh } from "cultmesh-ts";
|
||||
import type {
|
||||
CultMeshOperationContext,
|
||||
CultMeshQueryContext,
|
||||
CultMeshSurfaceCatalogIndexDiagnostic,
|
||||
CultMeshVerse,
|
||||
} from "cultmesh-ts";
|
||||
import type {
|
||||
CultNetDocumentPutRawMessage,
|
||||
CultNetPeer,
|
||||
} from "cultnet-ts";
|
||||
import {
|
||||
AetheriaRtsSchemas,
|
||||
createAetheriaRuntimeRtsOperationHandles,
|
||||
createAetheriaRuntimeRtsVerseFacade,
|
||||
createAetheriaRuntimeRtsQueryHandles,
|
||||
createAetheriaRuntimeRtsStatePointers,
|
||||
describeAetheriaRuntimeRtsLiveFeedSurface,
|
||||
describeAetheriaRuntimeRtsQueryHandles,
|
||||
describeAetheriaRuntimeRtsSurfaceCatalog,
|
||||
type AetheriaRuntimeDaemonCommandReceipt,
|
||||
type AuthorityStatusProjection,
|
||||
type DaemonHealthProjection,
|
||||
type GravityViewportResponse,
|
||||
type InventoryProjection,
|
||||
type ObjectsViewportResponse,
|
||||
type AetheriaRuntimeRtsLiveFeedDiagnostic,
|
||||
type AetheriaRuntimeRtsProjectionDiagnostic,
|
||||
type AetheriaRuntimeRtsSurfaceCatalogDiagnostic,
|
||||
type AetheriaRuntimeRtsOperationHandles,
|
||||
type AetheriaRuntimeRtsVerseFacade,
|
||||
type AetheriaRuntimeRtsStatePointers,
|
||||
type AetheriaRuntimeViewportFeedRequest,
|
||||
type AetheriaRuntimeViewportFeedSnapshot,
|
||||
type RtsSetMoveVectorRequest,
|
||||
type RtsSetTargetRequest,
|
||||
type SelectedObjectProjection,
|
||||
type SelectedObjectRequest,
|
||||
type StarbridgeSessionProjection,
|
||||
type ViewportRequest,
|
||||
type ViewportResponse,
|
||||
} from "./aetheria-rts-bindings.js";
|
||||
import { AetheriaLocalPublicationReader } from "./aetheria-local-publication-reader.js";
|
||||
import { AetheriaRemotePublicationReader } from "./aetheria-remote-publication-reader.js";
|
||||
import {
|
||||
projectAuthorityStatus,
|
||||
projectDaemonHealth,
|
||||
projectGravityViewportFromFrame,
|
||||
projectInventoryFromFrame,
|
||||
projectObjectsViewportFromFrame,
|
||||
projectSelectedObjectFromFrame,
|
||||
projectStarbridgeSessionSummary,
|
||||
projectViewportFromFrame,
|
||||
} from "./aetheria-rts-local-projection.js";
|
||||
|
||||
const connectionId = 0x43554c54;
|
||||
|
||||
type AetheriaPublicationReader = {
|
||||
readonly statePathDescription: string;
|
||||
readDaemonFrame(): Promise<unknown>;
|
||||
readDaemonHealth(): Promise<unknown>;
|
||||
readAuthorityPolicy(): Promise<unknown>;
|
||||
readStarbridgeSessionSummary(): Promise<unknown>;
|
||||
close?(): Promise<void>;
|
||||
};
|
||||
|
||||
export type AetheriaCultMeshClientOptions = {
|
||||
publicationMode?: "local" | "remote";
|
||||
snapshotTimeoutMs?: number;
|
||||
};
|
||||
|
||||
export type {
|
||||
BodyView,
|
||||
EntityStatus,
|
||||
GravityViewportResponse,
|
||||
GravityInfluence,
|
||||
InventoryItem,
|
||||
ObjectsViewportResponse,
|
||||
AetheriaRuntimeViewportFeedRequest,
|
||||
AetheriaRuntimeViewportFeedSnapshot,
|
||||
RtsSetMoveVectorRequest,
|
||||
RtsSetTargetRequest,
|
||||
SelectedObjectProjection,
|
||||
SelectedObjectRequest,
|
||||
StarbridgeSessionProjection,
|
||||
ViewObject,
|
||||
ViewportRequest,
|
||||
ViewportResponse,
|
||||
} from "./aetheria-rts-bindings.js";
|
||||
|
||||
export class AetheriaCultMeshClient {
|
||||
#peer: CultNetPeer | null = null;
|
||||
private readonly verse: CultMeshVerse;
|
||||
private readonly queryVerse: CultMeshVerse;
|
||||
private readonly commandVerse: CultMeshVerse;
|
||||
|
||||
public constructor(
|
||||
private readonly endpoint: string,
|
||||
statePath: string,
|
||||
private readonly runtimeId = "aetheria-rts-electron",
|
||||
options: AetheriaCultMeshClientOptions = {},
|
||||
) {
|
||||
const publicationMode = options.publicationMode ?? "local";
|
||||
this.publications = publicationMode === "remote"
|
||||
? new AetheriaRemotePublicationReader(
|
||||
this.endpoint,
|
||||
`${this.runtimeId}-reader`,
|
||||
options.snapshotTimeoutMs)
|
||||
: new AetheriaLocalPublicationReader(statePath);
|
||||
this.verse = CultMesh.verse("aetheria.local", this.runtimeId);
|
||||
this.queryVerse = publicationMode === "remote"
|
||||
? this.verse.withRoute("network", this.publications.statePathDescription)
|
||||
: this.verse.withRoute("shared-memory", this.publications.statePathDescription);
|
||||
this.commandVerse = this.verse
|
||||
.withRoute("network", this.endpoint)
|
||||
.withClaim("commander-control", { shardId: "aetheria.local" });
|
||||
const executors = {
|
||||
mapViewport: async (request: ViewportRequest) => projectViewportFromFrame(await this.fetchLatestFrameDocument(), request),
|
||||
objectsViewport: async (request: ViewportRequest) => projectObjectsViewportFromFrame(await this.fetchLatestFrameDocument(), request),
|
||||
gravityViewport: async (request: ViewportRequest) => projectGravityViewportFromFrame(await this.fetchLatestFrameDocument(), request),
|
||||
selectedObject: async (request: SelectedObjectRequest) => projectSelectedObjectFromFrame(await this.fetchLatestFrameDocument(), request),
|
||||
inventory: async (request: SelectedObjectRequest) => projectInventoryFromFrame(await this.fetchLatestFrameDocument(), request),
|
||||
daemonHealth: async () => projectDaemonHealth(await this.fetchDaemonHealthDocument()),
|
||||
authorityStatus: async () => projectAuthorityStatus(await this.fetchAuthorityPolicyDocument()),
|
||||
starbridgeSession: async () => projectStarbridgeSessionSummary(await this.fetchStarbridgeSessionSummaryDocument()),
|
||||
};
|
||||
this.queries = createAetheriaRuntimeRtsQueryHandles(
|
||||
executors,
|
||||
this.queryVerse.context.routeHint,
|
||||
{
|
||||
objectsViewport: CultMesh.pollingQueryWatcher(executors.objectsViewport, { intervalMs: 50 }),
|
||||
gravityViewport: CultMesh.pollingQueryWatcher(executors.gravityViewport, { intervalMs: 50 }),
|
||||
selectedObject: CultMesh.pollingQueryWatcher(executors.selectedObject, { intervalMs: 50 }),
|
||||
inventory: CultMesh.pollingQueryWatcher(executors.inventory, { intervalMs: 50 }),
|
||||
daemonHealth: CultMesh.pollingQueryWatcher(executors.daemonHealth, { intervalMs: 250 }),
|
||||
authorityStatus: CultMesh.pollingQueryWatcher(executors.authorityStatus, { intervalMs: 250 }),
|
||||
starbridgeSession: CultMesh.pollingQueryWatcher(executors.starbridgeSession, { intervalMs: 250 }),
|
||||
},
|
||||
);
|
||||
this.statePointers = createAetheriaRuntimeRtsStatePointers(
|
||||
this.queryVerse.context.routeHint,
|
||||
{
|
||||
daemonFrame: async () => this.fetchLatestFrameDocument(),
|
||||
daemonHealth: async () => this.fetchDaemonHealthDocument(),
|
||||
authorityPolicy: async () => this.fetchAuthorityPolicyDocument(),
|
||||
starbridgeSession: async () => this.fetchStarbridgeSessionSummaryDocument(),
|
||||
},
|
||||
);
|
||||
this.operations = createAetheriaRuntimeRtsOperationHandles(
|
||||
(commandId, issuedAtUtc, command, context) =>
|
||||
this.sendCommandDocument(commandId, issuedAtUtc, command, context),
|
||||
);
|
||||
this.aetheria = createAetheriaRuntimeRtsVerseFacade(
|
||||
this.queryVerse.context,
|
||||
this.commandVerse.context,
|
||||
this.queries,
|
||||
this.operations,
|
||||
this.statePointers,
|
||||
);
|
||||
}
|
||||
|
||||
private readonly publications: AetheriaPublicationReader;
|
||||
private readonly queries: ReturnType<typeof createAetheriaRuntimeRtsQueryHandles>;
|
||||
private readonly statePointers: AetheriaRuntimeRtsStatePointers;
|
||||
private readonly operations: AetheriaRuntimeRtsOperationHandles;
|
||||
private readonly aetheria: AetheriaRuntimeRtsVerseFacade;
|
||||
|
||||
public async close(): Promise<void> {
|
||||
await this.publications.close?.();
|
||||
this.#peer?.close();
|
||||
this.#peer = null;
|
||||
}
|
||||
|
||||
public async waitForFrame(timeoutMs: number): Promise<void> {
|
||||
const started = Date.now();
|
||||
let lastError = "";
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
try {
|
||||
await this.mapViewport(CultMesh.viewportRequest(CultMesh.rectFromBounds(-1000, -1000, 1000, 1000)));
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error.message : String(error);
|
||||
await delay(250);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for Aetheria CultMesh frame at ${this.endpoint}. ${lastError}`);
|
||||
}
|
||||
|
||||
public async mapViewport(request: ViewportRequest): Promise<ViewportResponse> {
|
||||
return this.aetheria.zone().viewport.within(request);
|
||||
}
|
||||
|
||||
public async objectsViewport(request: ViewportRequest): Promise<ObjectsViewportResponse> {
|
||||
return this.aetheria.zone().objects.visibleWithin(request);
|
||||
}
|
||||
|
||||
public async gravityViewport(request: ViewportRequest): Promise<GravityViewportResponse> {
|
||||
return this.aetheria.zone().gravity.within(request);
|
||||
}
|
||||
|
||||
public async selectedObject(request: SelectedObjectRequest): Promise<SelectedObjectProjection> {
|
||||
return this.aetheria.selectedObject(request.entityIndex);
|
||||
}
|
||||
|
||||
public async inventory(request: SelectedObjectRequest): Promise<InventoryProjection> {
|
||||
return this.aetheria.inventory(request.entityIndex);
|
||||
}
|
||||
|
||||
public async daemonHealth(): Promise<DaemonHealthProjection> {
|
||||
return this.aetheria.daemon.health();
|
||||
}
|
||||
|
||||
public async authorityStatus(): Promise<AuthorityStatusProjection> {
|
||||
return this.aetheria.daemon.authorityStatus();
|
||||
}
|
||||
|
||||
public async starbridgeSession(): Promise<StarbridgeSessionProjection> {
|
||||
return this.aetheria.daemon.starbridgeSession();
|
||||
}
|
||||
|
||||
public projectionDiagnostics(): Readonly<Record<string, AetheriaRuntimeRtsProjectionDiagnostic>> {
|
||||
return describeAetheriaRuntimeRtsQueryHandles(this.queries);
|
||||
}
|
||||
|
||||
public liveFeedDiagnostics(): Readonly<Record<string, AetheriaRuntimeRtsLiveFeedDiagnostic>> {
|
||||
return {
|
||||
viewportFeed: describeAetheriaRuntimeRtsLiveFeedSurface(this.createViewportFeed()),
|
||||
};
|
||||
}
|
||||
|
||||
public surfaceCatalogDiagnostics(): AetheriaRuntimeRtsSurfaceCatalogDiagnostic {
|
||||
const catalog = describeAetheriaRuntimeRtsSurfaceCatalog(this.queries, this.operations, this.statePointers);
|
||||
return CultMesh.describeSurfaceCatalog(catalog.catalogId, [
|
||||
...catalog.surfaces,
|
||||
CultMesh.describeSurface(this.createViewportFeed()),
|
||||
]);
|
||||
}
|
||||
|
||||
public surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic {
|
||||
return CultMesh.surfaceCatalogIndex(this.surfaceCatalogDiagnostics());
|
||||
}
|
||||
|
||||
public watchViewportFeed(
|
||||
request: AetheriaRuntimeViewportFeedRequest,
|
||||
callback: (snapshot: AetheriaRuntimeViewportFeedSnapshot) => void,
|
||||
): () => void {
|
||||
return this.createViewportFeed().watch(request, this.queryContext(), callback);
|
||||
}
|
||||
|
||||
public async setMoveVector(request: RtsSetMoveVectorRequest): Promise<AetheriaRuntimeDaemonCommandReceipt> {
|
||||
return this.aetheria.entity(request.actorEntityKey).pilot.move(
|
||||
CultMesh.vec2(request.directionX, request.directionY),
|
||||
{
|
||||
scalar: request.scalar,
|
||||
observedFrameId: request.observedFrameId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async setTarget(request: RtsSetTargetRequest): Promise<AetheriaRuntimeDaemonCommandReceipt> {
|
||||
return this.aetheria.entity(request.actorEntityKey).pilot.target(request.targetEntityKey, {
|
||||
observedFrameId: request.observedFrameId,
|
||||
});
|
||||
}
|
||||
|
||||
private async fetchLatestFrameDocument(): Promise<unknown> {
|
||||
return this.publications.readDaemonFrame();
|
||||
}
|
||||
|
||||
private async fetchDaemonHealthDocument(): Promise<unknown> {
|
||||
return this.publications.readDaemonHealth();
|
||||
}
|
||||
|
||||
private async fetchAuthorityPolicyDocument(): Promise<unknown> {
|
||||
return this.publications.readAuthorityPolicy();
|
||||
}
|
||||
|
||||
private async fetchStarbridgeSessionSummaryDocument(): Promise<unknown> {
|
||||
return this.publications.readStarbridgeSessionSummary();
|
||||
}
|
||||
|
||||
private createViewportFeed() {
|
||||
return CultMesh.liveFeed<AetheriaRuntimeViewportFeedRequest, AetheriaRuntimeViewportFeedSnapshot>(
|
||||
"gamecult.aetheria.rts.viewport_feed.v1",
|
||||
async (request, context) => {
|
||||
const objects = await this.queries.objectsViewport.execute(request.viewport, context);
|
||||
return this.createViewportFeedSnapshot(request, objects);
|
||||
},
|
||||
{
|
||||
sources: [
|
||||
...this.queries.objectsViewport.sources,
|
||||
...this.queries.gravityViewport.sources,
|
||||
...this.queries.daemonHealth.sources,
|
||||
...this.queries.authorityStatus.sources,
|
||||
...this.queries.starbridgeSession.sources,
|
||||
],
|
||||
routeHint: this.queryVerse.context.routeHint,
|
||||
watchFeed: (request, context, callback) => {
|
||||
let disposed = false;
|
||||
let sampling = false;
|
||||
const unsubscribe = this.queries.objectsViewport.watch(request.viewport, context, objects => {
|
||||
if (disposed || sampling) {
|
||||
return;
|
||||
}
|
||||
|
||||
sampling = true;
|
||||
void this.createViewportFeedSnapshot(request, objects)
|
||||
.then(snapshot => {
|
||||
if (!disposed) {
|
||||
callback(snapshot);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
sampling = false;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
unsubscribe();
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async createViewportFeedSnapshot(
|
||||
request: AetheriaRuntimeViewportFeedRequest,
|
||||
objects: ObjectsViewportResponse,
|
||||
): Promise<AetheriaRuntimeViewportFeedSnapshot> {
|
||||
const startedAt = performance.now();
|
||||
const selectedEntityIndex = request.selectedEntityIndex ?? -1;
|
||||
const [
|
||||
gravity,
|
||||
daemonHealth,
|
||||
authorityStatus,
|
||||
starbridgeSession,
|
||||
selectedObject,
|
||||
inventory,
|
||||
] = await Promise.all([
|
||||
this.aetheria.zone().gravity.within(request.viewport),
|
||||
this.aetheria.daemon.health(),
|
||||
this.aetheria.daemon.authorityStatus(),
|
||||
this.aetheria.daemon.starbridgeSession(),
|
||||
selectedEntityIndex >= 0
|
||||
? this.aetheria.selectedObject(selectedEntityIndex)
|
||||
: Promise.resolve(null),
|
||||
selectedEntityIndex >= 0
|
||||
? this.aetheria.inventory(selectedEntityIndex)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
return {
|
||||
viewport: composeViewport(objects, gravity),
|
||||
selectedObject,
|
||||
inventory,
|
||||
daemonHealth,
|
||||
authorityStatus,
|
||||
starbridgeSession,
|
||||
receivedAtUtc: new Date().toISOString(),
|
||||
sampleMs: performance.now() - startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private queryContext(): CultMeshQueryContext {
|
||||
return this.queryVerse.queryContext();
|
||||
}
|
||||
|
||||
private async sendCommandDocument(
|
||||
commandId: string,
|
||||
issuedAtUtc: string,
|
||||
command: unknown[],
|
||||
context: CultMeshOperationContext,
|
||||
): Promise<void> {
|
||||
const message: CultNetDocumentPutRawMessage = {
|
||||
schemaVersion: "cultnet.document_put_raw.v0",
|
||||
messageId: commandId,
|
||||
document: {
|
||||
schemaId: AetheriaRtsSchemas.daemonCommand,
|
||||
recordKey: `daemon:commands:${stableToken(commandId)}:${AetheriaRtsSchemas.daemonCommand}`,
|
||||
storedAt: issuedAtUtc,
|
||||
payloadEncoding: "messagepack",
|
||||
payload: encode(command),
|
||||
sourceRuntimeId: context.runtimeId,
|
||||
sourceRole: "rts-client",
|
||||
tags: ["aetheria-rts"],
|
||||
},
|
||||
};
|
||||
const peer = await this.peer();
|
||||
peer.send(message);
|
||||
await delay(80);
|
||||
}
|
||||
|
||||
private async peer(): Promise<CultNetPeer> {
|
||||
if (this.#peer)
|
||||
return this.#peer;
|
||||
this.#peer = await CultMesh.createRudpPeer(this.runtimeId, connectionId, this.endpoint, {
|
||||
connectTimeoutMs: 2000,
|
||||
maxFragmentBytes: 1200,
|
||||
maxPendingReliablePackets: 512,
|
||||
});
|
||||
this.#peer.on("close", () => {
|
||||
this.#peer = null;
|
||||
});
|
||||
return this.#peer;
|
||||
}
|
||||
}
|
||||
|
||||
function stableToken(value: string): string {
|
||||
return value
|
||||
.replace(/[^a-zA-Z0-9]+/gu, "-")
|
||||
.replace(/^-+|-+$/gu, "")
|
||||
.replace(/--+/gu, "-")
|
||||
.toLowerCase() || "empty";
|
||||
}
|
||||
|
||||
function composeViewport(
|
||||
objects: ObjectsViewportResponse,
|
||||
gravity: GravityViewportResponse,
|
||||
): ViewportResponse {
|
||||
return {
|
||||
schema: "gamecult.aetheria.rts_viewport.v1",
|
||||
frameId: objects.frameId,
|
||||
publishedAtUtc: objects.publishedAtUtc,
|
||||
simulationTimeSeconds: objects.simulationTimeSeconds,
|
||||
runId: objects.runId,
|
||||
zoneIndex: objects.zoneIndex,
|
||||
zoneName: objects.zoneName,
|
||||
currentEntityKey: objects.currentEntityKey,
|
||||
viewport: objects.viewport,
|
||||
controlledEntityIndices: objects.controlledEntityIndices,
|
||||
objects: objects.objects,
|
||||
gravityInfluences: gravity.gravityInfluences,
|
||||
bodies: gravity.bodies,
|
||||
};
|
||||
}
|
||||
|
||||
function delay(milliseconds: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { decode } from "@msgpack/msgpack";
|
||||
import { SingleFileMessagePackBackingStore } from "cultcache-ts";
|
||||
|
||||
export class AetheriaLocalPublicationReader {
|
||||
public constructor(private readonly statePath: string) {}
|
||||
|
||||
public get statePathDescription(): string {
|
||||
return this.statePath;
|
||||
}
|
||||
|
||||
public readDaemonFrame(): Promise<unknown> {
|
||||
return this.readSingleDocument(`${this.statePath}.daemon.frame.cc`, "gamecult.aetheria.daemon_frame.v1");
|
||||
}
|
||||
|
||||
public readDaemonHealth(): Promise<unknown> {
|
||||
return this.readSingleDocument(`${this.statePath}.daemon.health.cc`, "gamecult.aetheria.daemon_health.v1");
|
||||
}
|
||||
|
||||
public readAuthorityPolicy(): Promise<unknown> {
|
||||
return this.readSingleDocument(`${this.statePath}.authority.policy.cc`, "gamecult.aetheria.verse_authority_policy.v1");
|
||||
}
|
||||
|
||||
public readStarbridgeSessionSummary(): Promise<unknown> {
|
||||
return this.readSingleDocument(
|
||||
`${this.statePath}.daemon.starbridge.session.cc`,
|
||||
"gamecult.aetheria.starbridge_session_summary.v1");
|
||||
}
|
||||
|
||||
private async readSingleDocument(path: string, schemaId: string): Promise<unknown> {
|
||||
const records = await new SingleFileMessagePackBackingStore(path).pullAll();
|
||||
const record = records.find(candidate => candidate.schemaId === schemaId);
|
||||
if (!record)
|
||||
throw new Error(`Aetheria local publication ${path} did not contain schema ${schemaId}.`);
|
||||
return decode(record.payload);
|
||||
}
|
||||
}
|
||||
139
Aetheria.Rts.Web/Electron/aetheria-remote-publication-reader.ts
Normal file
139
Aetheria.Rts.Web/Electron/aetheria-remote-publication-reader.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { decode } from "@msgpack/msgpack";
|
||||
import { CultMesh } from "cultmesh-ts";
|
||||
import type { CultNetPeer } from "cultnet-ts";
|
||||
import type {
|
||||
CultNetErrorMessage,
|
||||
CultNetSnapshotResponseRawMessage,
|
||||
} from "cultnet-ts";
|
||||
|
||||
const connectionId = 0x43554c54;
|
||||
|
||||
export class AetheriaRemotePublicationReader {
|
||||
#peer: CultNetPeer | null = null;
|
||||
|
||||
public constructor(
|
||||
private readonly endpoint: string,
|
||||
private readonly runtimeId = "aetheria-rts-remote-reader",
|
||||
private readonly timeoutMs = 2_500,
|
||||
) {}
|
||||
|
||||
public get statePathDescription(): string {
|
||||
return this.endpoint;
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
this.#peer?.close();
|
||||
this.#peer = null;
|
||||
}
|
||||
|
||||
public readDaemonFrame(): Promise<unknown> {
|
||||
return this.readSingleDocument(
|
||||
"gamecult.aetheria.daemon_frame.v1",
|
||||
"daemon:aetheria.frame.latest.v1");
|
||||
}
|
||||
|
||||
public readDaemonHealth(): Promise<unknown> {
|
||||
return this.readSingleDocument(
|
||||
"gamecult.aetheria.daemon_health.v1",
|
||||
"daemon:aetheria.health.v1");
|
||||
}
|
||||
|
||||
public readAuthorityPolicy(): Promise<unknown> {
|
||||
return this.readSingleDocument(
|
||||
"gamecult.aetheria.verse_authority_policy.v1",
|
||||
"global:aetheria.verse_authority_policy.v1");
|
||||
}
|
||||
|
||||
public readStarbridgeSessionSummary(): Promise<unknown> {
|
||||
return this.readSingleDocument(
|
||||
"gamecult.aetheria.starbridge_session_summary.v1",
|
||||
"daemon:aetheria.starbridge.session.latest.v1");
|
||||
}
|
||||
|
||||
private async readSingleDocument(schemaId: string, recordKey: string): Promise<unknown> {
|
||||
const response = await this.requestSnapshot([recordKey]);
|
||||
const document = response.documents.find(candidate =>
|
||||
candidate.recordKey === recordKey);
|
||||
if (!document) {
|
||||
throw new Error(`Aetheria remote publication ${this.endpoint} did not return ${schemaId} at ${recordKey}.`);
|
||||
}
|
||||
|
||||
return decode(toBytes(document.payload));
|
||||
}
|
||||
|
||||
private async requestSnapshot(
|
||||
recordKeys: readonly string[],
|
||||
): Promise<CultNetSnapshotResponseRawMessage> {
|
||||
const peer = await this.peer();
|
||||
const messageId = `${this.runtimeId}:snapshot:${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
return new Promise<CultNetSnapshotResponseRawMessage>((resolve, reject) => {
|
||||
const cleanup = (): void => {
|
||||
clearTimeout(timer);
|
||||
peer.off("message", onMessage);
|
||||
peer.off("invalidMessage", onInvalidMessage);
|
||||
peer.off("error", onError);
|
||||
peer.off("close", onClose);
|
||||
};
|
||||
const rejectWith = (error: Error): void => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
};
|
||||
const onMessage = (message: unknown): void => {
|
||||
const response = message as CultNetSnapshotResponseRawMessage | CultNetErrorMessage;
|
||||
if (response.schemaVersion === "cultnet.error.v0") {
|
||||
cleanup();
|
||||
reject(new Error(response.error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.schemaVersion !== "cultnet.snapshot_response_raw.v0" ||
|
||||
response.messageId !== messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
resolve(response);
|
||||
};
|
||||
const onInvalidMessage = (error: Error): void => rejectWith(error);
|
||||
const onError = (error: Error): void => rejectWith(error);
|
||||
const onClose = (): void => rejectWith(new Error("CultMesh peer closed before snapshot response."));
|
||||
const timer = setTimeout(
|
||||
() => rejectWith(new Error(`Timed out waiting for Aetheria remote snapshot ${messageId}.`)),
|
||||
this.timeoutMs);
|
||||
|
||||
peer.on("message", onMessage);
|
||||
peer.on("invalidMessage", onInvalidMessage);
|
||||
peer.on("error", onError);
|
||||
peer.on("close", onClose);
|
||||
peer.sendSnapshotRequest({
|
||||
schemaVersion: "cultnet.snapshot_request.v0",
|
||||
messageId,
|
||||
recordKeys: [...recordKeys],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async peer(): Promise<CultNetPeer> {
|
||||
if (this.#peer)
|
||||
return this.#peer;
|
||||
|
||||
this.#peer = await CultMesh.createRudpPeer(this.runtimeId, connectionId, this.endpoint, {
|
||||
connectTimeoutMs: 2_000,
|
||||
maxFragmentBytes: 1200,
|
||||
maxPendingReliablePackets: 512,
|
||||
});
|
||||
this.#peer.on("close", () => {
|
||||
this.#peer = null;
|
||||
});
|
||||
return this.#peer;
|
||||
}
|
||||
}
|
||||
|
||||
function toBytes(value: unknown): Uint8Array {
|
||||
if (value instanceof Uint8Array)
|
||||
return value;
|
||||
if (Array.isArray(value))
|
||||
return Uint8Array.from(value);
|
||||
throw new Error("CultNet raw document payload was not binary.");
|
||||
}
|
||||
50
Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts
Normal file
50
Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import type { CultMeshViewportRequest } from "cultmesh-ts";
|
||||
|
||||
export {
|
||||
AetheriaRtsIpcChannels,
|
||||
AetheriaRtsSchemas,
|
||||
AetheriaRuntimeDaemonCommandKinds,
|
||||
createAetheriaRuntimeRtsOperationHandles,
|
||||
createAetheriaRuntimeRtsQueryHandles,
|
||||
createAetheriaRuntimeRtsStatePointers,
|
||||
createAetheriaRuntimeRtsVerseFacade,
|
||||
describeAetheriaRuntimeRtsLiveFeedSurface,
|
||||
describeAetheriaRuntimeRtsQueryHandles,
|
||||
describeAetheriaRuntimeRtsSurfaceCatalog,
|
||||
} from "./aetheria-rts-generated-bindings.js";
|
||||
|
||||
export type {
|
||||
AetheriaRuntimeDaemonCommandReceipt,
|
||||
AetheriaRuntimeRtsLiveFeedDiagnostic,
|
||||
AetheriaRuntimeRtsOperationHandles,
|
||||
AetheriaRuntimeRtsProjectionDiagnostic,
|
||||
AetheriaRuntimeRtsStatePointerResolvers,
|
||||
AetheriaRuntimeRtsStatePointers,
|
||||
AetheriaRuntimeRtsSurfaceCatalogDiagnostic,
|
||||
AetheriaRuntimeRtsVerseFacade,
|
||||
AetheriaRuntimeSelectedObjectRequest as SelectedObjectRequest,
|
||||
AetheriaRuntimeSetMoveVectorRequest as RtsSetMoveVectorRequest,
|
||||
AetheriaRuntimeSetTargetRequest as RtsSetTargetRequest,
|
||||
AetheriaRuntimeViewportFeedRequest,
|
||||
AetheriaRuntimeViewportFeedSnapshot,
|
||||
AuthorityRuleProjection,
|
||||
AuthorityStatusProjection,
|
||||
BodyView,
|
||||
DaemonHealthProjection,
|
||||
EntityStatus,
|
||||
GravityInfluence,
|
||||
GravityViewportResponse,
|
||||
InventoryItem,
|
||||
InventoryProjection,
|
||||
ObjectsViewportResponse,
|
||||
SelectedObjectProjection,
|
||||
StarbridgeBaseStatusProjection,
|
||||
StarbridgeRuntimeRoleProjection,
|
||||
StarbridgeSessionProjection,
|
||||
StarbridgeStationStockItemProjection,
|
||||
StarbridgeWaveForecastProjection,
|
||||
ViewObject,
|
||||
ViewportResponse,
|
||||
} from "./aetheria-rts-generated-bindings.js";
|
||||
|
||||
export type ViewportRequest = CultMeshViewportRequest;
|
||||
2132
Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts
Normal file
2132
Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts
Normal file
File diff suppressed because it is too large
Load diff
445
Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts
Normal file
445
Aetheria.Rts.Web/Electron/aetheria-rts-local-projection.ts
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
import {
|
||||
AetheriaRtsSchemas,
|
||||
aetheriaRuntimeAuthorityRuleSlots as authorityRuleSlots,
|
||||
aetheriaRuntimeBodySnapshotCommitSlots as bodySlots,
|
||||
aetheriaRuntimeCargoBayLoadoutCommitSlots as cargoBaySlots,
|
||||
aetheriaRuntimeDaemonFrameDocumentSlots as frameSlots,
|
||||
aetheriaRuntimeDaemonHealthDocumentSlots as healthSlots,
|
||||
aetheriaRuntimeEntitySnapshotCommitSlots as entitySlots,
|
||||
aetheriaRuntimeEntityStatGridCommitSlots as statGridSlots,
|
||||
aetheriaRuntimeLoadoutItemCommitSlots as itemSlots,
|
||||
aetheriaRuntimeLoadoutItemSlotCommitSlots as itemSlotSlots,
|
||||
aetheriaRuntimeRunCheckpointCommitSlots as runSlots,
|
||||
aetheriaRuntimeStarbridgeBaseStatusSlots as starbridgeBaseSlots,
|
||||
aetheriaRuntimeStarbridgeRuntimeRoleSlots as starbridgeRoleSlots,
|
||||
aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots as starbridgeSummarySlots,
|
||||
aetheriaRuntimeStarbridgeStationStockItemSlots as starbridgeStockSlots,
|
||||
aetheriaRuntimeStarbridgeWaveForecastSlots as starbridgeWaveForecastSlots,
|
||||
aetheriaRuntimeVerseAuthorityPolicyDocumentSlots as authorityPolicySlots,
|
||||
aetheriaRuntimeZoneSnapshotCommitSlots as zoneSlots,
|
||||
} from "./aetheria-rts-generated-bindings.js";
|
||||
import { cultMeshRectFromBounds, cultMeshViewportRequest } from "cultmesh-ts";
|
||||
import type {
|
||||
AuthorityStatusProjection,
|
||||
BodyView,
|
||||
DaemonHealthProjection,
|
||||
GravityViewportResponse,
|
||||
GravityInfluence,
|
||||
InventoryProjection,
|
||||
InventoryItem,
|
||||
ObjectsViewportResponse,
|
||||
SelectedObjectProjection,
|
||||
SelectedObjectRequest,
|
||||
StarbridgeSessionProjection,
|
||||
ViewObject,
|
||||
ViewportRequest,
|
||||
ViewportResponse,
|
||||
} from "./aetheria-rts-bindings.js";
|
||||
|
||||
export function projectViewportFromFrame(frameDocument: unknown, request: ViewportRequest): ViewportResponse {
|
||||
const objects = projectObjectsViewportFromFrame(frameDocument, request);
|
||||
const gravity = projectGravityViewportFromFrame(frameDocument, request);
|
||||
return {
|
||||
schema: AetheriaRtsSchemas.rtsViewport,
|
||||
frameId: objects.frameId,
|
||||
publishedAtUtc: objects.publishedAtUtc,
|
||||
simulationTimeSeconds: objects.simulationTimeSeconds,
|
||||
runId: objects.runId,
|
||||
zoneIndex: objects.zoneIndex,
|
||||
zoneName: objects.zoneName,
|
||||
currentEntityKey: objects.currentEntityKey,
|
||||
viewport: objects.viewport,
|
||||
controlledEntityIndices: objects.controlledEntityIndices,
|
||||
objects: objects.objects,
|
||||
gravityInfluences: gravity.gravityInfluences,
|
||||
bodies: gravity.bodies,
|
||||
};
|
||||
}
|
||||
|
||||
export function projectObjectsViewportFromFrame(
|
||||
frameDocument: unknown,
|
||||
request: ViewportRequest,
|
||||
): ObjectsViewportResponse {
|
||||
const frame = arr(frameDocument);
|
||||
const run = arr(frame[frameSlots.run]);
|
||||
const zones = list<unknown[]>(run[runSlots.zones]);
|
||||
const currentZoneIndex = num(run[runSlots.currentZoneIndex], -1);
|
||||
const zone = zones.find(candidate => num(candidate[zoneSlots.zoneIndex], -1) === currentZoneIndex) ??
|
||||
zones[0] ??
|
||||
[];
|
||||
const runId = str(run[runSlots.runId]) || "local-rts";
|
||||
const viewport = normalizeViewport(request);
|
||||
const entities = list<unknown[]>(zone[zoneSlots.entities]);
|
||||
const controlledEntityIndices = entities
|
||||
.filter(isPlayerControlled)
|
||||
.map(entity => num(entity[entitySlots.entityIndex], -1))
|
||||
.filter(index => index >= 0);
|
||||
const controlled = entities.filter(entity => controlledEntityIndices.includes(num(entity[entitySlots.entityIndex], -1)));
|
||||
const objects = entities
|
||||
.filter(entity => entityIntersectsViewport(entity, viewport))
|
||||
.filter(entity => isPlayerControlled(entity) ||
|
||||
controlled.length === 0 ||
|
||||
controlled.some(observer => canSee(observer, entity)))
|
||||
.map(entity => toViewObject(entity, runId, num(zone[zoneSlots.zoneIndex])));
|
||||
|
||||
return {
|
||||
schema: AetheriaRtsSchemas.objectsViewport,
|
||||
frameId: num(frame[frameSlots.frameId]),
|
||||
publishedAtUtc: str(frame[frameSlots.publishedAtUtc]),
|
||||
simulationTimeSeconds: num(frame[frameSlots.simulationTimeSeconds]),
|
||||
runId,
|
||||
zoneIndex: num(zone[zoneSlots.zoneIndex]),
|
||||
zoneName: str(zone[zoneSlots.name]) || `Zone ${num(zone[zoneSlots.zoneIndex])}`,
|
||||
currentEntityKey: str(run[runSlots.currentEntityKey]),
|
||||
viewport,
|
||||
controlledEntityIndices,
|
||||
objects,
|
||||
};
|
||||
}
|
||||
|
||||
export function projectGravityViewportFromFrame(
|
||||
frameDocument: unknown,
|
||||
request: ViewportRequest,
|
||||
): GravityViewportResponse {
|
||||
const frame = arr(frameDocument);
|
||||
const run = arr(frame[frameSlots.run]);
|
||||
const zones = list<unknown[]>(run[runSlots.zones]);
|
||||
const currentZoneIndex = num(run[runSlots.currentZoneIndex], -1);
|
||||
const zone = zones.find(candidate => num(candidate[zoneSlots.zoneIndex], -1) === currentZoneIndex) ??
|
||||
zones[0] ??
|
||||
[];
|
||||
const runId = str(run[runSlots.runId]) || "local-rts";
|
||||
const viewport = normalizeViewport(request);
|
||||
const visibleBodies = list<unknown[]>(zone[zoneSlots.bodies])
|
||||
.filter(body => gravityInfluenceIntersectsViewport(body, viewport));
|
||||
|
||||
return {
|
||||
schema: AetheriaRtsSchemas.gravityViewport,
|
||||
frameId: num(frame[frameSlots.frameId]),
|
||||
publishedAtUtc: str(frame[frameSlots.publishedAtUtc]),
|
||||
simulationTimeSeconds: num(frame[frameSlots.simulationTimeSeconds]),
|
||||
runId,
|
||||
zoneIndex: num(zone[zoneSlots.zoneIndex]),
|
||||
zoneName: str(zone[zoneSlots.name]) || `Zone ${num(zone[zoneSlots.zoneIndex])}`,
|
||||
viewport,
|
||||
gravityInfluences: visibleBodies.map(toGravityInfluence),
|
||||
bodies: visibleBodies.map(toBodyView),
|
||||
};
|
||||
}
|
||||
|
||||
export function projectSelectedObjectFromFrame(
|
||||
frameDocument: unknown,
|
||||
request: SelectedObjectRequest,
|
||||
): SelectedObjectProjection {
|
||||
const context = frameContext(frameDocument);
|
||||
const entity = context.entities.find(candidate => num(candidate[entitySlots.entityIndex], -1) === request.entityIndex);
|
||||
return {
|
||||
schema: AetheriaRtsSchemas.selectedObject,
|
||||
frameId: context.frameId,
|
||||
runId: context.runId,
|
||||
zoneIndex: context.zoneIndex,
|
||||
entityIndex: request.entityIndex,
|
||||
selected: entity ? toViewObject(entity, context.runId, context.zoneIndex) : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function projectInventoryFromFrame(frameDocument: unknown, request: SelectedObjectRequest): InventoryProjection {
|
||||
const context = frameContext(frameDocument);
|
||||
const entity = context.entities.find(candidate => num(candidate[entitySlots.entityIndex], -1) === request.entityIndex);
|
||||
const allItems = entity ? inventory(entity) : [];
|
||||
return {
|
||||
schema: AetheriaRtsSchemas.inventory,
|
||||
frameId: context.frameId,
|
||||
runId: context.runId,
|
||||
zoneIndex: context.zoneIndex,
|
||||
entityIndex: request.entityIndex,
|
||||
entityKey: entity ? entityKey(context.runId, context.zoneIndex, request.entityIndex) : "",
|
||||
items: allItems,
|
||||
equipment: allItems.filter(item => item.source === "equipment"),
|
||||
cargo: allItems.filter(item => item.source === "cargo"),
|
||||
};
|
||||
}
|
||||
|
||||
export function projectDaemonHealth(healthDocument: unknown): DaemonHealthProjection {
|
||||
const health = arr(healthDocument);
|
||||
return {
|
||||
schema: str(health[healthSlots.schema]) || AetheriaRtsSchemas.daemonHealth,
|
||||
daemonId: str(health[healthSlots.daemonId]),
|
||||
verseId: str(health[healthSlots.verseId]),
|
||||
publishedAtUtc: str(health[healthSlots.publishedAtUtc]),
|
||||
statePath: str(health[healthSlots.statePath]),
|
||||
frameId: num(health[healthSlots.frameId]),
|
||||
observedCommandCount: num(health[healthSlots.observedCommandCount]),
|
||||
appliedCommandCount: num(health[healthSlots.appliedCommandCount]),
|
||||
rejectedCommandCount: num(health[healthSlots.rejectedCommandCount]),
|
||||
status: str(health[healthSlots.status]) || "unknown",
|
||||
publicationSource: str(health[healthSlots.publicationSource]),
|
||||
transport: str(health[healthSlots.transport]),
|
||||
commandBoundaryPath: str(health[healthSlots.commandBoundaryPath]),
|
||||
};
|
||||
}
|
||||
|
||||
export function projectAuthorityStatus(policyDocument: unknown): AuthorityStatusProjection {
|
||||
const policy = arr(policyDocument);
|
||||
return {
|
||||
schema: str(policy[authorityPolicySlots.schema]) || AetheriaRtsSchemas.verseAuthorityPolicy,
|
||||
verseId: str(policy[authorityPolicySlots.verseId]),
|
||||
policyId: str(policy[authorityPolicySlots.policyId]),
|
||||
ruleVersion: str(policy[authorityPolicySlots.ruleVersion]),
|
||||
hostRuntimeId: str(policy[authorityPolicySlots.hostRuntimeId]),
|
||||
defaultMode: str(policy[authorityPolicySlots.defaultMode]),
|
||||
updatedAtUtc: str(policy[authorityPolicySlots.updatedAtUtc]),
|
||||
rules: list<unknown[]>(policy[authorityPolicySlots.rules]).map(rule => ({
|
||||
ruleId: str(rule[authorityRuleSlots.ruleId]),
|
||||
subjectPrefix: str(rule[authorityRuleSlots.subjectPrefix]),
|
||||
claimKinds: stringList(rule[authorityRuleSlots.claimKinds]),
|
||||
mode: str(rule[authorityRuleSlots.mode]),
|
||||
runtimeIds: stringList(rule[authorityRuleSlots.runtimeIds]),
|
||||
leaseScope: str(rule[authorityRuleSlots.leaseScope]),
|
||||
priority: num(rule[authorityRuleSlots.priority]),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function projectStarbridgeSessionSummary(summaryDocument: unknown): StarbridgeSessionProjection {
|
||||
const summary = arr(summaryDocument);
|
||||
return {
|
||||
schema: str(summary[starbridgeSummarySlots.schema]) || AetheriaRtsSchemas.starbridgeSessionSummary,
|
||||
frameId: num(summary[starbridgeSummarySlots.frameId]),
|
||||
publishedAtUtc: str(summary[starbridgeSummarySlots.publishedAtUtc]),
|
||||
sessionId: str(summary[starbridgeSummarySlots.sessionId]) || "starbridge-session",
|
||||
scenarioId: str(summary[starbridgeSummarySlots.scenarioId]) || "starbridge.local",
|
||||
scenarioName: str(summary[starbridgeSummarySlots.scenarioName]) || "Starbridge",
|
||||
runId: str(summary[starbridgeSummarySlots.runId]) || "local-starbridge",
|
||||
zoneIndex: num(summary[starbridgeSummarySlots.zoneIndex]),
|
||||
zoneName: str(summary[starbridgeSummarySlots.zoneName]),
|
||||
phase: str(summary[starbridgeSummarySlots.phase]) || "setup",
|
||||
currentWaveIndex: num(summary[starbridgeSummarySlots.currentWaveIndex]),
|
||||
baseStatus: toStarbridgeBaseStatus(arr(summary[starbridgeSummarySlots.baseStatus])),
|
||||
stationStock: list<unknown[]>(summary[starbridgeSummarySlots.stationStock]).map(toStarbridgeStationStockItem),
|
||||
waveForecast: list<unknown[]>(summary[starbridgeSummarySlots.waveForecast]).map(toStarbridgeWaveForecast),
|
||||
runtimeRoles: list<unknown[]>(summary[starbridgeSummarySlots.runtimeRoles]).map(toStarbridgeRuntimeRole),
|
||||
};
|
||||
}
|
||||
|
||||
function frameContext(frameDocument: unknown): {
|
||||
frameId: number;
|
||||
runId: string;
|
||||
zoneIndex: number;
|
||||
entities: unknown[][];
|
||||
} {
|
||||
const frame = arr(frameDocument);
|
||||
const run = arr(frame[frameSlots.run]);
|
||||
const zones = list<unknown[]>(run[runSlots.zones]);
|
||||
const currentZoneIndex = num(run[runSlots.currentZoneIndex], -1);
|
||||
const zone = zones.find(candidate => num(candidate[zoneSlots.zoneIndex], -1) === currentZoneIndex) ??
|
||||
zones[0] ??
|
||||
[];
|
||||
return {
|
||||
frameId: num(frame[frameSlots.frameId]),
|
||||
runId: str(run[runSlots.runId]) || "local-rts",
|
||||
zoneIndex: num(zone[zoneSlots.zoneIndex]),
|
||||
entities: list<unknown[]>(zone[zoneSlots.entities]),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeViewport(request: ViewportRequest): ViewportRequest {
|
||||
return cultMeshViewportRequest(
|
||||
cultMeshRectFromBounds(request.minX, request.minY, request.maxX, request.maxY),
|
||||
request.controlledEntityIndices,
|
||||
);
|
||||
}
|
||||
|
||||
function toViewObject(entity: unknown[], runId: string, zoneIndex: number): ViewObject {
|
||||
return {
|
||||
entityIndex: num(entity[entitySlots.entityIndex], -1),
|
||||
entityKey: entityKey(runId, zoneIndex, num(entity[entitySlots.entityIndex], -1)),
|
||||
displayName: str(entity[entitySlots.name]),
|
||||
kind: str(entity[entitySlots.kind]),
|
||||
factionKey: str(entity[entitySlots.factionKey]),
|
||||
x: num(entity[entitySlots.positionX]),
|
||||
y: num(entity[entitySlots.positionZ]),
|
||||
z: num(entity[entitySlots.positionY]),
|
||||
directionX: num(entity[entitySlots.directionX]),
|
||||
directionY: num(entity[entitySlots.directionY]),
|
||||
velocityX: num(entity[entitySlots.velocityX]),
|
||||
velocityY: num(entity[entitySlots.velocityY]),
|
||||
controlled: isPlayerControlled(entity),
|
||||
targetEntityIndex: num(entity[entitySlots.targetEntityIndex], -1),
|
||||
isActive: bool(entity[entitySlots.isActive]),
|
||||
visibility: num(entity[entitySlots.visibility]),
|
||||
status: {
|
||||
hull: stat(entity, "hull"),
|
||||
shield: stat(entity, "shield"),
|
||||
heat: stat(entity, "heat"),
|
||||
},
|
||||
inventory: inventory(entity),
|
||||
};
|
||||
}
|
||||
|
||||
function toStarbridgeBaseStatus(status: unknown[]): StarbridgeSessionProjection["baseStatus"] {
|
||||
return {
|
||||
entityKey: str(status[starbridgeBaseSlots.entityKey]),
|
||||
displayName: str(status[starbridgeBaseSlots.displayName]),
|
||||
hull: num(status[starbridgeBaseSlots.hull]),
|
||||
shield: num(status[starbridgeBaseSlots.shield]),
|
||||
heat: num(status[starbridgeBaseSlots.heat]),
|
||||
isActive: bool(status[starbridgeBaseSlots.isActive]),
|
||||
};
|
||||
}
|
||||
|
||||
function toStarbridgeStationStockItem(item: unknown[]): StarbridgeSessionProjection["stationStock"][number] {
|
||||
return {
|
||||
itemKey: str(item[starbridgeStockSlots.itemKey]),
|
||||
quantity: num(item[starbridgeStockSlots.quantity]),
|
||||
quality: num(item[starbridgeStockSlots.quality]),
|
||||
durability: num(item[starbridgeStockSlots.durability]),
|
||||
source: str(item[starbridgeStockSlots.source]) || "station",
|
||||
};
|
||||
}
|
||||
|
||||
function toStarbridgeWaveForecast(wave: unknown[]): StarbridgeSessionProjection["waveForecast"][number] {
|
||||
return {
|
||||
waveIndex: num(wave[starbridgeWaveForecastSlots.waveIndex]),
|
||||
displayName: str(wave[starbridgeWaveForecastSlots.displayName]),
|
||||
attackerKeys: stringList(wave[starbridgeWaveForecastSlots.attackerKeys]),
|
||||
bossKey: str(wave[starbridgeWaveForecastSlots.bossKey]),
|
||||
recoveredTechnologyKeys: stringList(wave[starbridgeWaveForecastSlots.recoveredTechnologyKeys]),
|
||||
};
|
||||
}
|
||||
|
||||
function toStarbridgeRuntimeRole(role: unknown[]): StarbridgeSessionProjection["runtimeRoles"][number] {
|
||||
return {
|
||||
runtimeId: str(role[starbridgeRoleSlots.runtimeId]),
|
||||
role: str(role[starbridgeRoleSlots.role]),
|
||||
entityKey: str(role[starbridgeRoleSlots.entityKey]),
|
||||
};
|
||||
}
|
||||
|
||||
function toGravityInfluence(body: unknown[]): GravityInfluence {
|
||||
return {
|
||||
bodyKey: str(body[bodySlots.bodyKey]),
|
||||
orbitKey: str(body[bodySlots.orbitKey]),
|
||||
kind: str(body[bodySlots.kind]),
|
||||
x: num(body[bodySlots.gravityInfluenceCenterX]),
|
||||
y: num(body[bodySlots.gravityInfluenceCenterZ]),
|
||||
radius: resolveGravityRadius(body),
|
||||
gravityDepth: num(body[bodySlots.gravityWellDepth]),
|
||||
gravityDepthExponent: num(body[bodySlots.gravityDepthExponent]),
|
||||
waveRadius: num(body[bodySlots.gravityWaveRadius]),
|
||||
waveDepth: num(body[bodySlots.gravityWaveDepth]),
|
||||
waveSpeed: num(body[bodySlots.gravityWaveSpeed]),
|
||||
};
|
||||
}
|
||||
|
||||
function toBodyView(body: unknown[]): BodyView {
|
||||
return {
|
||||
bodyKey: str(body[bodySlots.bodyKey]),
|
||||
orbitKey: str(body[bodySlots.orbitKey]),
|
||||
name: str(body[bodySlots.name]),
|
||||
kind: str(body[bodySlots.kind]),
|
||||
x: num(body[bodySlots.gravityInfluenceCenterX]),
|
||||
y: num(body[bodySlots.gravityInfluenceCenterZ]),
|
||||
radius: Math.max(32, num(body[bodySlots.bodyRadiusMultiplier]) * 70),
|
||||
isAsteroidBelt: str(body[bodySlots.kind]).toLowerCase().includes("asteroid"),
|
||||
};
|
||||
}
|
||||
|
||||
function inventory(entity: unknown[]): InventoryItem[] {
|
||||
const items: InventoryItem[] = [];
|
||||
for (const slot of list<unknown[]>(entity[entitySlots.equipment]))
|
||||
addSlot(items, "equipment", slot);
|
||||
for (const bay of list<unknown[]>(entity[entitySlots.cargoContents])) {
|
||||
for (const slot of list<unknown[]>(bay[cargoBaySlots.items]))
|
||||
addSlot(items, "cargo", slot);
|
||||
}
|
||||
|
||||
return items.filter(item => item.itemKey.length > 0);
|
||||
}
|
||||
|
||||
function addSlot(items: InventoryItem[], source: string, slot: unknown[]): void {
|
||||
const item = arr(slot[itemSlotSlots.item]);
|
||||
items.push({
|
||||
source,
|
||||
itemKey: str(item[itemSlots.itemKey]),
|
||||
quantity: num(item[itemSlots.quantity]),
|
||||
quality: num(item[itemSlots.quality]),
|
||||
durability: num(item[itemSlots.durability]),
|
||||
enabled: bool(item[itemSlots.enabled]),
|
||||
});
|
||||
}
|
||||
|
||||
function entityIntersectsViewport(entity: unknown[], viewport: ViewportRequest): boolean {
|
||||
const x = num(entity[entitySlots.positionX]);
|
||||
const y = num(entity[entitySlots.positionZ]);
|
||||
return x >= viewport.minX && x <= viewport.maxX && y >= viewport.minY && y <= viewport.maxY;
|
||||
}
|
||||
|
||||
function gravityInfluenceIntersectsViewport(body: unknown[], viewport: ViewportRequest): boolean {
|
||||
const x = num(body[bodySlots.gravityInfluenceCenterX]);
|
||||
const y = num(body[bodySlots.gravityInfluenceCenterZ]);
|
||||
const radius = resolveGravityRadius(body);
|
||||
return x + radius >= viewport.minX &&
|
||||
x - radius <= viewport.maxX &&
|
||||
y + radius >= viewport.minY &&
|
||||
y - radius <= viewport.maxY;
|
||||
}
|
||||
|
||||
function canSee(observer: unknown[], target: unknown[]): boolean {
|
||||
if (num(observer[entitySlots.entityIndex], -1) === num(target[entitySlots.entityIndex], -2))
|
||||
return true;
|
||||
|
||||
const dx = num(observer[entitySlots.positionX]) - num(target[entitySlots.positionX]);
|
||||
const dy = num(observer[entitySlots.positionZ]) - num(target[entitySlots.positionZ]);
|
||||
const range = Math.max(180, num(observer[entitySlots.visibility]));
|
||||
return dx * dx + dy * dy <= range * range;
|
||||
}
|
||||
|
||||
function isPlayerControlled(entity: unknown[]): boolean {
|
||||
return str(entity[entitySlots.factionKey]).toLowerCase() === "player";
|
||||
}
|
||||
|
||||
function stat(entity: unknown[], name: string): number {
|
||||
const grid = list<unknown[]>(entity[entitySlots.statGrids])
|
||||
.find(candidate => str(candidate[statGridSlots.name]).toLowerCase() === name.toLowerCase());
|
||||
return grid ? numberList(grid[statGridSlots.values])[0] ?? 0 : 0;
|
||||
}
|
||||
|
||||
function resolveGravityRadius(body: unknown[]): number {
|
||||
const explicit = num(body[bodySlots.gravityInfluenceRadius]);
|
||||
if (explicit > 0)
|
||||
return explicit;
|
||||
return Math.max(32, num(body[bodySlots.bodyRadiusMultiplier]) * 70);
|
||||
}
|
||||
|
||||
function entityKey(runId: string, zoneIndex: number, entityIndex: number): string {
|
||||
return `global:aetheria.run_state.${runId}.zone.${zoneIndex}.entity.${entityIndex}.v1`;
|
||||
}
|
||||
|
||||
function arr(value: unknown): unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function list<T extends unknown[]>(value: unknown): T[] {
|
||||
return Array.isArray(value) ? value.filter(Array.isArray) as T[] : [];
|
||||
}
|
||||
|
||||
function numberList(value: unknown): number[] {
|
||||
return Array.isArray(value) ? value.map(candidate => num(candidate)) : [];
|
||||
}
|
||||
|
||||
function stringList(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map(candidate => str(candidate)).filter(candidate => candidate.length > 0) : [];
|
||||
}
|
||||
|
||||
function str(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function num(value: unknown, fallback = 0): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function bool(value: unknown): boolean {
|
||||
return value === true;
|
||||
}
|
||||
424
Aetheria.Rts.Web/Electron/main.ts
Normal file
424
Aetheria.Rts.Web/Electron/main.ts
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
import { app, BrowserWindow, ipcMain, shell } from "electron";
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { createWriteStream, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
AetheriaCultMeshClient,
|
||||
} from "./aetheria-cultmesh.js";
|
||||
import { registerAetheriaRtsIpcHandlers } from "./aetheria-rts-generated-bindings.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = resolve(__dirname, "..");
|
||||
const repoRoot = resolve(projectRoot, "..");
|
||||
const logsRoot = resolve(projectRoot, "logs");
|
||||
const runtimeRoot = resolve(process.env.AETHERIA_RTS_RUNTIME_ROOT ?? resolve(projectRoot, "runtime"));
|
||||
const runtimeStatePath = resolve(runtimeRoot, "aetheria-rts.cc");
|
||||
const daemonDll = resolve(repoRoot, "Aetheria.State.Daemon", "bin", "Debug", "net10.0", "Aetheria.State.Daemon.dll");
|
||||
const rendererIndex = resolve(projectRoot, "wwwroot", "index.html");
|
||||
const rtsCultMeshPort = Number.parseInt(process.env.AETHERIA_RTS_CULTMESH_PORT ?? "3076", 10);
|
||||
const configuredDaemonEndpoint = process.env.AETHERIA_RTS_DAEMON_ENDPOINT?.trim() ?? "";
|
||||
const launchLocalDaemon = configuredDaemonEndpoint.length === 0;
|
||||
const rtsCultMeshEndpoint = configuredDaemonEndpoint || `rudp://127.0.0.1:${rtsCultMeshPort}`;
|
||||
const rtsVerseId = process.env.AETHERIA_RTS_VERSE_ID?.trim() || "aetheria.local";
|
||||
const rtsDaemonId = process.env.AETHERIA_RTS_DAEMON_ID?.trim() || "starfire-rts";
|
||||
const rtsCultMeshAdvertiseHost = process.env.AETHERIA_RTS_CULTMESH_ADVERTISE_HOST?.trim() || "127.0.0.1";
|
||||
const peerCultMeshEndpoints = parseEndpointList(process.env.AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS);
|
||||
const electronSmoke = process.env.AETHERIA_RTS_ELECTRON_SMOKE === "1";
|
||||
const electronSmokeResultPath = process.env.AETHERIA_RTS_ELECTRON_SMOKE_RESULT;
|
||||
|
||||
let daemonProcess: ChildProcessWithoutNullStreams | null = null;
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let rtsClient: AetheriaCultMeshClient | null = null;
|
||||
let isQuitting = false;
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
mainWindow = createWindow();
|
||||
registerIpc();
|
||||
showStartup("Preparing Aetheria RTS", "Building daemon if needed.");
|
||||
|
||||
try {
|
||||
if (launchLocalDaemon) {
|
||||
await ensureDotnetBuild();
|
||||
showStartup("Launching Aetheria RTS", "Starting the Aetheria daemon.");
|
||||
mkdirSync(runtimeRoot, { recursive: true });
|
||||
daemonProcess = startDotnet("aetheria-daemon", daemonDll, [
|
||||
"--state",
|
||||
runtimeStatePath,
|
||||
"--verse-id",
|
||||
rtsVerseId,
|
||||
"--daemon-id",
|
||||
rtsDaemonId,
|
||||
"--rts-cultmesh-port",
|
||||
rtsCultMeshPort.toString(),
|
||||
"--rts-cultmesh-advertise-host",
|
||||
rtsCultMeshAdvertiseHost,
|
||||
"--tick-interval-ms",
|
||||
"20",
|
||||
"--fixed-delta-ms",
|
||||
"20",
|
||||
"--api-publication-interval-ms",
|
||||
"100000",
|
||||
...peerCultMeshEndpoints.flatMap(endpoint => ["--peer-cultmesh-endpoint", endpoint]),
|
||||
]);
|
||||
} else {
|
||||
showStartup("Connecting Aetheria RTS", `Using daemon ${rtsCultMeshEndpoint}.`);
|
||||
mkdirSync(runtimeRoot, { recursive: true });
|
||||
}
|
||||
|
||||
rtsClient = new AetheriaCultMeshClient(
|
||||
rtsCultMeshEndpoint,
|
||||
runtimeStatePath,
|
||||
"aetheria-rts-electron",
|
||||
{ publicationMode: launchLocalDaemon ? "local" : "remote" });
|
||||
|
||||
showStartup("Launching Aetheria RTS", "Waiting for the daemon CultMesh frame.");
|
||||
await rtsClient.waitForFrame(30000);
|
||||
await mainWindow.loadFile(rendererIndex);
|
||||
if (electronSmoke) {
|
||||
const result = await runElectronSmoke(mainWindow);
|
||||
writeElectronSmokeResult({ ok: true, result });
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
exitElectronSmoke(0);
|
||||
}
|
||||
} catch (error) {
|
||||
await showFailure(error);
|
||||
if (electronSmoke) {
|
||||
writeElectronSmokeResult({
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.stack ?? error.message : String(error),
|
||||
});
|
||||
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
||||
exitElectronSmoke(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
isQuitting = true;
|
||||
void rtsClient?.close();
|
||||
stopChild(daemonProcess);
|
||||
});
|
||||
|
||||
function createWindow(): BrowserWindow {
|
||||
const window = new BrowserWindow({
|
||||
width: 1440,
|
||||
height: 900,
|
||||
minWidth: 1000,
|
||||
minHeight: 640,
|
||||
show: !electronSmoke,
|
||||
backgroundColor: "#0b1016",
|
||||
title: "Aetheria RTS",
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
preload: resolve(__dirname, "preload.cjs"),
|
||||
},
|
||||
});
|
||||
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
void shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, unknown>> {
|
||||
const started = Date.now();
|
||||
let lastResult: Record<string, unknown> | null = null;
|
||||
while (Date.now() - started < 15000) {
|
||||
lastResult = await window.webContents.executeJavaScript(`
|
||||
(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
const api = window.aetheriaRts;
|
||||
const status = document.querySelector("#status")?.textContent ?? "";
|
||||
const selectionTitle = document.querySelector("#selection-title")?.textContent ?? "";
|
||||
const selectionDetails = document.querySelector("#selection-details")?.textContent ?? "";
|
||||
const frameDetails = document.querySelector("#frame-details")?.textContent ?? "";
|
||||
const runtimeSurfaceDetails = document.querySelector("#runtime-surface-details")?.textContent ?? "";
|
||||
const controlledText = document.querySelector("#controlled-list")?.textContent ?? "";
|
||||
const canvas = document.querySelector("#map");
|
||||
const health = api ? await api.daemonHealth() : null;
|
||||
const authority = api ? await api.authorityStatus() : null;
|
||||
const starbridge = api ? await api.starbridgeSession() : null;
|
||||
const surfaceCatalog = api ? await api.surfaceCatalog() : null;
|
||||
const surfaceCatalogIndex = api ? await api.surfaceCatalogIndex() : null;
|
||||
const viewport = api ? await api.mapViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 }) : null;
|
||||
const actor = viewport?.objects?.find(object => object.controlled) ?? viewport?.objects?.[0] ?? null;
|
||||
const moveReceipt = api && actor ? await api.setMoveVector({
|
||||
actorEntityKey: actor.entityKey,
|
||||
directionX: 1,
|
||||
directionY: 0,
|
||||
scalar: 0.1,
|
||||
observedFrameId: viewport.frameId,
|
||||
}) : null;
|
||||
return {
|
||||
hasApi: !!api &&
|
||||
typeof api.mapViewport === "function" &&
|
||||
typeof api.objectsViewport === "function" &&
|
||||
typeof api.gravityViewport === "function" &&
|
||||
typeof api.selectedObject === "function" &&
|
||||
typeof api.inventory === "function" &&
|
||||
typeof api.daemonHealth === "function" &&
|
||||
typeof api.authorityStatus === "function" &&
|
||||
typeof api.starbridgeSession === "function" &&
|
||||
typeof api.surfaceCatalog === "function" &&
|
||||
typeof api.surfaceCatalogIndex === "function",
|
||||
status,
|
||||
selectionTitle,
|
||||
selectionDetails,
|
||||
frameDetails,
|
||||
runtimeSurfaceDetails,
|
||||
controlledText,
|
||||
canvasClientWidth: canvas?.clientWidth ?? 0,
|
||||
canvasClientHeight: canvas?.clientHeight ?? 0,
|
||||
canvasWidth: canvas?.width ?? 0,
|
||||
canvasHeight: canvas?.height ?? 0,
|
||||
health,
|
||||
authority,
|
||||
starbridge,
|
||||
surfaceCatalog,
|
||||
surfaceCatalogIndex,
|
||||
moveReceipt
|
||||
};
|
||||
})()
|
||||
`, true) as Record<string, unknown>;
|
||||
|
||||
if (isElectronSmokeReady(lastResult))
|
||||
return lastResult;
|
||||
await delay(250);
|
||||
}
|
||||
|
||||
throw new Error(`Aetheria RTS Electron smoke did not reach ready state. Last result: ${JSON.stringify(lastResult)}`);
|
||||
}
|
||||
|
||||
function isElectronSmokeReady(result: Record<string, unknown>): boolean {
|
||||
const status = stringValue(result.status);
|
||||
const selectionTitle = stringValue(result.selectionTitle);
|
||||
const selectionDetails = stringValue(result.selectionDetails);
|
||||
const frameDetails = stringValue(result.frameDetails);
|
||||
const runtimeSurfaceDetails = stringValue(result.runtimeSurfaceDetails);
|
||||
const controlledText = stringValue(result.controlledText);
|
||||
const health = objectValue(result.health);
|
||||
const authority = objectValue(result.authority);
|
||||
const starbridge = objectValue(result.starbridge);
|
||||
const surfaceCatalog = objectValue(result.surfaceCatalog);
|
||||
const surfaceCatalogIndex = objectValue(result.surfaceCatalogIndex);
|
||||
const moveReceipt = objectValue(result.moveReceipt);
|
||||
return result.hasApi === true &&
|
||||
status.includes("frame") &&
|
||||
selectionTitle.length > 0 &&
|
||||
selectionTitle !== "No Selection" &&
|
||||
selectionDetails.includes("Equipment") &&
|
||||
selectionDetails.includes("Cargo") &&
|
||||
frameDetails.includes("Daemon") &&
|
||||
frameDetails.includes("Authority") &&
|
||||
runtimeSurfaceDetails.includes("shared-memory") &&
|
||||
runtimeSurfaceDetails.includes("daemon:aetheria.frame.latest.v1") &&
|
||||
runtimeSurfaceDetails.includes("gamecult.aetheria.pilot.set_move_vector.v1") &&
|
||||
controlledText.length > 0 &&
|
||||
numberValue(result.canvasClientWidth) > 0 &&
|
||||
numberValue(result.canvasClientHeight) > 0 &&
|
||||
numberValue(result.canvasWidth) > 0 &&
|
||||
numberValue(result.canvasHeight) > 0 &&
|
||||
health?.status === "healthy" &&
|
||||
authority?.policyId === "aetheria.trusted-coop.v1" &&
|
||||
starbridge?.scenarioName === "Frontier Fabricator Defense" &&
|
||||
surfaceCatalog?.catalogId === "gamecult.aetheria.rts.surfaces.v1" &&
|
||||
arrayValue(surfaceCatalogIndex?.queries).length > 0 &&
|
||||
arrayValue(surfaceCatalogIndex?.operations).some(surface =>
|
||||
objectValue(surface)?.surfaceId === "gamecult.aetheria.pilot.set_move_vector.v1") &&
|
||||
stringValue(moveReceipt?.commandId).length > 0 &&
|
||||
stringValue(moveReceipt?.operationId) === "gamecult.aetheria.pilot.set_move_vector.v1" &&
|
||||
objectValue(moveReceipt?.route)?.kind === "network" &&
|
||||
moveReceipt?.accepted === true;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
function objectValue(value: unknown): Record<string, unknown> | null {
|
||||
return value != null && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function arrayValue(value: unknown): unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function writeElectronSmokeResult(result: Record<string, unknown>): void {
|
||||
if (!electronSmokeResultPath)
|
||||
return;
|
||||
|
||||
writeFileSync(electronSmokeResultPath, JSON.stringify(result, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function exitElectronSmoke(exitCode: number): void {
|
||||
isQuitting = true;
|
||||
void rtsClient?.close();
|
||||
rtsClient = null;
|
||||
stopChild(daemonProcess);
|
||||
daemonProcess = null;
|
||||
mainWindow?.destroy();
|
||||
mainWindow = null;
|
||||
setImmediate(() => app.exit(exitCode));
|
||||
}
|
||||
|
||||
function registerIpc(): void {
|
||||
registerAetheriaRtsIpcHandlers(
|
||||
ipcMain,
|
||||
requireClient,
|
||||
() => ({
|
||||
status: "ok",
|
||||
transport: "cultmesh-rudp",
|
||||
endpoint: rtsCultMeshEndpoint,
|
||||
verseId: rtsVerseId,
|
||||
daemonId: rtsDaemonId,
|
||||
peerEndpoints: peerCultMeshEndpoints,
|
||||
daemonRunning: daemonProcess != null && !daemonProcess.killed,
|
||||
daemonMode: launchLocalDaemon ? "local" : "remote",
|
||||
}));
|
||||
}
|
||||
|
||||
function parseEndpointList(value: string | undefined): string[] {
|
||||
if (!value)
|
||||
return [];
|
||||
|
||||
return value
|
||||
.split(/[;,]/u)
|
||||
.map(endpoint => endpoint.trim())
|
||||
.filter(endpoint => endpoint.length > 0);
|
||||
}
|
||||
|
||||
function requireClient(): AetheriaCultMeshClient {
|
||||
if (!rtsClient)
|
||||
throw new Error("Aetheria RTS CultMesh client is not initialized.");
|
||||
return rtsClient;
|
||||
}
|
||||
|
||||
async function ensureDotnetBuild(): Promise<void> {
|
||||
if (existsSync(daemonDll))
|
||||
return;
|
||||
|
||||
await runProcess("dotnet", ["build", resolve(repoRoot, "Aetheria.State.Daemon", "Aetheria.State.Daemon.csproj")], "Aetheria.State.Daemon.build");
|
||||
}
|
||||
|
||||
function startDotnet(name: string, dllPath: string, args: string[]): ChildProcessWithoutNullStreams {
|
||||
mkdirSync(logsRoot, { recursive: true });
|
||||
const child = spawn("dotnet", [dllPath, ...args], {
|
||||
cwd: repoRoot,
|
||||
windowsHide: true,
|
||||
});
|
||||
pipeChildLogs(name, child);
|
||||
child.once("exit", (code, signal) => {
|
||||
if (!isQuitting) {
|
||||
void showFailure(new Error(`${name} exited unexpectedly (${signal ?? code ?? "unknown"}). Check ${logsRoot}.`));
|
||||
}
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
function runProcess(command: string, args: string[], logName: string): Promise<void> {
|
||||
mkdirSync(logsRoot, { recursive: true });
|
||||
const child = spawn(command, args, {
|
||||
cwd: repoRoot,
|
||||
windowsHide: true,
|
||||
});
|
||||
pipeChildLogs(logName, child);
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("exit", code => {
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}. Check ${logsRoot}.`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pipeChildLogs(name: string, child: ChildProcessWithoutNullStreams): void {
|
||||
const stdout = createWriteStream(resolve(logsRoot, `${name}.log`), { flags: "a" });
|
||||
const stderr = createWriteStream(resolve(logsRoot, `${name}.err.log`), { flags: "a" });
|
||||
child.stdout.pipe(stdout);
|
||||
child.stderr.pipe(stderr);
|
||||
}
|
||||
|
||||
function stopChild(child: ChildProcessWithoutNullStreams | null): void {
|
||||
if (!child || child.killed)
|
||||
return;
|
||||
|
||||
child.kill();
|
||||
}
|
||||
|
||||
function delay(milliseconds: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
function showStartup(title: string, detail: string): void {
|
||||
void mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startupHtml(title, detail))}`);
|
||||
}
|
||||
|
||||
async function showFailure(error: unknown): Promise<void> {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startupHtml("Aetheria RTS failed to start", message))}`);
|
||||
}
|
||||
|
||||
function startupHtml(title: string, detail: string): string {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #0b1016;
|
||||
color: #dbe7f3;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
main {
|
||||
width: min(520px, calc(100vw - 48px));
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 24px;
|
||||
font-weight: 650;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: #8fa3b7;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>${escapeHtml(title)}</h1>
|
||||
<p>${escapeHtml(detail)}</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
53
Aetheria.Rts.Web/Electron/preload.cjs
Normal file
53
Aetheria.Rts.Web/Electron/preload.cjs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// <auto-generated />
|
||||
// Generated by scripts/generate-rts-bindings.mjs from the RTS IPC surface contract.
|
||||
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
const channels = {
|
||||
mapViewport: "aetheria-rts:map-viewport",
|
||||
objectsViewport: "aetheria-rts:objects-viewport",
|
||||
gravityViewport: "aetheria-rts:gravity-viewport",
|
||||
viewportFeed: "aetheria-rts:viewport-feed",
|
||||
viewportFeedStop: "aetheria-rts:viewport-feed-stop",
|
||||
viewportFeedUpdate: "aetheria-rts:viewport-feed-update",
|
||||
selectedObject: "aetheria-rts:selected-object",
|
||||
inventory: "aetheria-rts:inventory",
|
||||
daemonHealth: "aetheria-rts:daemon-health",
|
||||
authorityStatus: "aetheria-rts:authority-status",
|
||||
starbridgeSession: "aetheria-rts:starbridge-session",
|
||||
setMoveVector: "aetheria-rts:set-move-vector",
|
||||
setTarget: "aetheria-rts:set-target",
|
||||
surfaceCatalog: "aetheria-rts:surface-catalog",
|
||||
surfaceCatalogIndex: "aetheria-rts:surface-catalog-index",
|
||||
health: "aetheria-rts:health",
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld("aetheriaRts", {
|
||||
mapViewport: request => ipcRenderer.invoke(channels.mapViewport, request),
|
||||
objectsViewport: request => ipcRenderer.invoke(channels.objectsViewport, request),
|
||||
gravityViewport: request => ipcRenderer.invoke(channels.gravityViewport, request),
|
||||
watchViewportFeed: (request, callback) => {
|
||||
const subscriptionId = `viewport-feed-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
const listener = (_event, message) => {
|
||||
if (message?.subscriptionId === subscriptionId) {
|
||||
callback(message.snapshot);
|
||||
}
|
||||
};
|
||||
ipcRenderer.on(channels.viewportFeedUpdate, listener);
|
||||
void ipcRenderer.invoke(channels.viewportFeed, { subscriptionId, feed: request });
|
||||
return () => {
|
||||
ipcRenderer.off(channels.viewportFeedUpdate, listener);
|
||||
void ipcRenderer.invoke(channels.viewportFeedStop, subscriptionId);
|
||||
};
|
||||
},
|
||||
selectedObject: request => ipcRenderer.invoke(channels.selectedObject, request),
|
||||
inventory: request => ipcRenderer.invoke(channels.inventory, request),
|
||||
daemonHealth: () => ipcRenderer.invoke(channels.daemonHealth),
|
||||
authorityStatus: () => ipcRenderer.invoke(channels.authorityStatus),
|
||||
starbridgeSession: () => ipcRenderer.invoke(channels.starbridgeSession),
|
||||
setMoveVector: request => ipcRenderer.invoke(channels.setMoveVector, request),
|
||||
setTarget: request => ipcRenderer.invoke(channels.setTarget, request),
|
||||
surfaceCatalog: () => ipcRenderer.invoke(channels.surfaceCatalog),
|
||||
surfaceCatalogIndex: () => ipcRenderer.invoke(channels.surfaceCatalogIndex),
|
||||
health: () => ipcRenderer.invoke(channels.health),
|
||||
});
|
||||
275
Aetheria.Rts.Web/package-lock.json
generated
Normal file
275
Aetheria.Rts.Web/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
{
|
||||
"name": "aetheria-rts-web",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aetheria-rts-web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"cultcache-ts": "file:../../CultLib/packages/cultcache-ts",
|
||||
"cultmesh-ts": "file:../../CultLib/packages/cultmesh-ts",
|
||||
"cultnet-ts": "file:../../CultLib/packages/cultnet-ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.0.0",
|
||||
"electron": "^42.4.1",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"../../CultLib/packages/cultcache-ts": {
|
||||
"version": "0.13.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.3",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.9.0"
|
||||
}
|
||||
},
|
||||
"../../CultLib/packages/cultmesh-ts": {
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"cultcache-ts": "file:../cultcache-ts",
|
||||
"cultnet-ts": "file:../cultnet-ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"typescript": "^5.9.0",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
},
|
||||
"../../CultLib/packages/cultnet-ts": {
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.3",
|
||||
"ajv": "^8.17.1",
|
||||
"cultcache-ts": "file:../cultcache-ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"json-schema-to-typescript": "^15.0.4",
|
||||
"typescript": "^5.9.0",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-internal/extract-zip": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.3.tgz",
|
||||
"integrity": "sha512-OjKpjB7gohtEjZiq6nDx1egqjZJhGPN1iFOIED+NFhB/MMkXw/XRcHjh1DGXKT5z2W9eW7Jy2UKU3gpjvusFTQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz",
|
||||
"integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"env-paths": "^3.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"progress": "^2.0.3",
|
||||
"semver": "^7.6.3",
|
||||
"sumchecker": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"undici": "^7.24.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpack/msgpack": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
|
||||
"integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
|
||||
"integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cultcache-ts": {
|
||||
"resolved": "../../CultLib/packages/cultcache-ts",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/cultmesh-ts": {
|
||||
"resolved": "../../CultLib/packages/cultmesh-ts",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/cultnet-ts": {
|
||||
"resolved": "../../CultLib/packages/cultnet-ts",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "42.4.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-42.4.1.tgz",
|
||||
"integrity": "sha512-8CYHJP5O4wFO+ycoJR98yy907MmPeo+vWXrzjxmGGgRNKqv8pOjjm+wphO0CCgQJnBU7+QUPSJS4QXhbKrO50w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-internal/extract-zip": "^1.0.1",
|
||||
"@electron/get": "^5.0.0",
|
||||
"@types/node": "^24.9.0"
|
||||
},
|
||||
"bin": {
|
||||
"electron": "cli.js",
|
||||
"install-electron": "install.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "24.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||||
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
|
||||
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sumchecker": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
||||
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Aetheria.Rts.Web/package.json
Normal file
31
Aetheria.Rts.Web/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "aetheria-rts-web",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "electron-dist/main.js",
|
||||
"scripts": {
|
||||
"build": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json && C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.electron.json && C:\\Progra~1\\nodejs\\node.exe scripts/copy-electron-preload.mjs",
|
||||
"build:renderer": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json",
|
||||
"build:electron": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.electron.json && C:\\Progra~1\\nodejs\\node.exe scripts/copy-electron-preload.mjs",
|
||||
"generate:rts-bindings": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-rts-bindings.mjs",
|
||||
"check:rts-bindings": "C:\\Progra~1\\nodejs\\node.exe scripts/generate-rts-bindings.mjs --check",
|
||||
"verify:stage7b": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7b-rts-client.ps1",
|
||||
"verify:stage7c": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7c-local-runtime.ps1",
|
||||
"verify:stage7c:electron": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7c-electron-shell.ps1",
|
||||
"verify:stage7d:remote": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts\\verify-stage7d-remote-cultmesh.ps1",
|
||||
"electron": "npm run build && .\\node_modules\\electron\\dist\\electron.exe .",
|
||||
"watch": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.0.0",
|
||||
"electron": "^42.4.1",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"cultcache-ts": "file:../../CultLib/packages/cultcache-ts",
|
||||
"cultmesh-ts": "file:../../CultLib/packages/cultmesh-ts",
|
||||
"cultnet-ts": "file:../../CultLib/packages/cultnet-ts"
|
||||
}
|
||||
}
|
||||
10
Aetheria.Rts.Web/scripts/copy-electron-preload.mjs
Normal file
10
Aetheria.Rts.Web/scripts/copy-electron-preload.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { copyFileSync, rmSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const root = resolve(import.meta.dirname, "..");
|
||||
const source = resolve(root, "Electron", "preload.cjs");
|
||||
const legacyOutput = resolve(root, "electron-dist", "preload.js");
|
||||
const output = resolve(root, "electron-dist", "preload.cjs");
|
||||
|
||||
rmSync(legacyOutput, { force: true });
|
||||
copyFileSync(source, output);
|
||||
1273
Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs
Normal file
1273
Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs
Normal file
File diff suppressed because it is too large
Load diff
531
Aetheria.Rts.Web/scripts/verify-stage7b-rts-client.ps1
Normal file
531
Aetheria.Rts.Web/scripts/verify-stage7b-rts-client.ps1
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $root
|
||||
|
||||
& node scripts/generate-rts-bindings.mjs --check
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: generated RTS bindings are stale."
|
||||
}
|
||||
|
||||
$forbidden = @(
|
||||
'window\.aetheriaRts\.command',
|
||||
'window\.aetheriaRts\.viewport',
|
||||
'ipcMain\.handle\("aetheria-rts:command',
|
||||
'ipcMain\.handle\("aetheria-rts:viewport',
|
||||
'function createCommandDocument',
|
||||
'export type RtsCommandRequest',
|
||||
'public async command\(',
|
||||
'public async viewport\('
|
||||
)
|
||||
|
||||
$forbiddenPattern = ($forbidden -join '|')
|
||||
$forbiddenHits = & rg -n $forbiddenPattern Client Electron 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: public generic RTS command/viewport surface still exists.`n$forbiddenHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for forbidden public surfaces."
|
||||
}
|
||||
|
||||
$required = @(
|
||||
'mapViewport',
|
||||
'objectsViewport',
|
||||
'gravityViewport',
|
||||
'selectedObject',
|
||||
'inventory',
|
||||
'daemonHealth',
|
||||
'authorityStatus',
|
||||
'starbridgeSession',
|
||||
'setMoveVector',
|
||||
'setTarget',
|
||||
'surfaceCatalogIndex',
|
||||
'AetheriaRtsIpcChannels'
|
||||
)
|
||||
|
||||
foreach ($symbol in $required) {
|
||||
& rg -q $symbol Client Electron
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: expected typed RTS client symbol '$symbol' was not found."
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Get-Content Client/app.ts -Raw).Contains('window.aetheriaRts.surfaceCatalogIndex()')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS renderer does not read the shared CultMesh surface catalog index."
|
||||
}
|
||||
|
||||
$rendererText = Get-Content Client/app.ts -Raw
|
||||
if (-not $rendererText.Contains('surface.routeHint.kind') -or -not $rendererText.Contains('surface.sources.length')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS renderer does not expose CultMesh surface route and source metadata."
|
||||
}
|
||||
|
||||
if (-not $rendererText.Contains('latestOperationReceipt') -or
|
||||
-not $rendererText.Contains('receipt.commandId') -or
|
||||
-not $rendererText.Contains('receipt.operationId') -or
|
||||
-not $rendererText.Contains('receipt.accepted') -or
|
||||
-not $rendererText.Contains('receipt.route.kind')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS renderer does not expose CultMesh operation receipt metadata."
|
||||
}
|
||||
|
||||
if (-not (Get-Content wwwroot/index.html -Raw).Contains('runtime-surface-details')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS renderer does not expose runtime surface diagnostics in the shell."
|
||||
}
|
||||
|
||||
$transportLayoutPatterns = @(
|
||||
'\[[0-9]+\]',
|
||||
'new Array<unknown>',
|
||||
'command\[',
|
||||
'value\[',
|
||||
'bounds\[',
|
||||
'viewportSchema',
|
||||
'commandSchema',
|
||||
'createBaseCommandDocument',
|
||||
'decodeViewObject',
|
||||
'decodeInventoryItem',
|
||||
'decodeGravityInfluence',
|
||||
'decodeBodyView',
|
||||
'fetchViewportDocument',
|
||||
'fetchDocument\(',
|
||||
'sendSnapshotRequest',
|
||||
'schemaIds',
|
||||
'recordKeys',
|
||||
'CultNetSnapshotResponseRawMessage',
|
||||
'AetheriaRtsSchemas\.rtsViewport'
|
||||
)
|
||||
|
||||
$transportLayoutPattern = ($transportLayoutPatterns -join '|')
|
||||
$transportLayoutHits = & rg -n $transportLayoutPattern Electron/aetheria-cultmesh.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: RTS CultMesh transport wrapper still owns document layout/codec details.`n$transportLayoutHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for transport wrapper layout checks."
|
||||
}
|
||||
|
||||
$requiredBindingSymbols = @(
|
||||
'createAetheriaRuntimeRtsOperationHandles',
|
||||
'createAetheriaRuntimeRtsQueryHandles',
|
||||
'AetheriaRtsSchemas'
|
||||
)
|
||||
|
||||
foreach ($symbol in $requiredBindingSymbols) {
|
||||
& rg -q $symbol Electron/aetheria-rts-bindings.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: expected RTS binding symbol '$symbol' was not found."
|
||||
}
|
||||
}
|
||||
|
||||
$requiredGeneratedBindingSymbols = @(
|
||||
'AetheriaRtsIpcChannels',
|
||||
'registerAetheriaRtsIpcHandlers',
|
||||
'export type AetheriaRtsMainClient',
|
||||
'aetheria-rts:map-viewport',
|
||||
'aetheria-rts:objects-viewport',
|
||||
'aetheria-rts:viewport-feed',
|
||||
'aetheria-rts:viewport-feed-update',
|
||||
'aetheria-rts:set-move-vector',
|
||||
'export type AetheriaRuntimeSelectedObjectRequest',
|
||||
'export type AetheriaRuntimeViewportFeedRequest',
|
||||
'export type AetheriaRuntimeViewportFeedSnapshot',
|
||||
'aetheriaRuntimeDaemonCommandDocumentSlots',
|
||||
'encodeSetMoveVectorCommand',
|
||||
'encodeSetTargetCommand',
|
||||
'AetheriaRuntimeRtsProjectionSources',
|
||||
'AetheriaRuntimeRtsProjectionDiagnostic',
|
||||
'AetheriaRuntimeRtsLiveFeedDiagnostic',
|
||||
'AetheriaRuntimeRtsSurfaceCatalogDiagnostic',
|
||||
'AetheriaRuntimeRtsStatePointers',
|
||||
'AetheriaRuntimeRtsStatePointerResolvers',
|
||||
'CultMeshSurfaceCatalogDiagnostic',
|
||||
'CultMeshSurfaceCatalogIndexDiagnostic',
|
||||
'CultMeshOperationReceipt',
|
||||
'aetheria-rts:surface-catalog',
|
||||
'aetheria-rts:surface-catalog-index',
|
||||
'surfaceCatalogDiagnostics(): CultMeshSurfaceCatalogDiagnostic',
|
||||
'surfaceCatalogIndexDiagnostics(): CultMeshSurfaceCatalogIndexDiagnostic',
|
||||
'ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalog',
|
||||
'ipcMain.handle(AetheriaRtsIpcChannels.surfaceCatalogIndex',
|
||||
'CultMesh.projectionSource',
|
||||
'CultMesh.projectionRecipe',
|
||||
'CultMeshProjectionSource',
|
||||
'routeHint: CultMeshRouteHint',
|
||||
'CultMeshQueryWatcher',
|
||||
'AetheriaRuntimeRtsQueryWatchers',
|
||||
'createAetheriaRuntimeRtsStatePointers',
|
||||
'CultMesh.statePointer',
|
||||
'CultMesh.bindStatePointer',
|
||||
'CultMesh.describeSurface(statePointers.daemonFrame)',
|
||||
'routeHint, watchProjection',
|
||||
'watchProjection: watchers.objectsViewport',
|
||||
'.asQuerySurface()',
|
||||
'describeAetheriaRuntimeRtsQueryHandles',
|
||||
'describeAetheriaRuntimeRtsLiveFeedSurface',
|
||||
'describeAetheriaRuntimeRtsSurfaceCatalog',
|
||||
'CultMesh.describeQuerySurface',
|
||||
'CultMesh.describeLiveFeed',
|
||||
'CultMesh.describeSurfaceCatalog',
|
||||
'CultMesh.describeSurface',
|
||||
'CultMesh.describeSurface(operations.setMoveVector)',
|
||||
'CultMesh.describeSurface(operations.setTarget)',
|
||||
'CultMesh.operation',
|
||||
'CultMesh.operationReceipt',
|
||||
'AetheriaRuntimeRtsOperationHandles',
|
||||
'AetheriaRuntimeRtsVerseFacade',
|
||||
'createAetheriaRuntimeRtsVerseFacade',
|
||||
'CultMesh.bindQuery',
|
||||
'CultMesh.bindOperation',
|
||||
'zone: (_zoneId =',
|
||||
'visibleWithin',
|
||||
'entity: (entityKey: string)',
|
||||
'pilot',
|
||||
'selectedObject: CultMesh.projectionRecipe',
|
||||
'inventory: CultMesh.projectionRecipe',
|
||||
'daemonHealth: CultMesh.projectionRecipe',
|
||||
'authorityStatus: CultMesh.projectionRecipe',
|
||||
'starbridgeSession: CultMesh.projectionRecipe'
|
||||
)
|
||||
|
||||
foreach ($symbol in $requiredGeneratedBindingSymbols) {
|
||||
& rg -F -q $symbol Electron/aetheria-rts-generated-bindings.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: expected generated RTS binding symbol '$symbol' was not found."
|
||||
}
|
||||
}
|
||||
|
||||
$forbiddenGeneratedQuerySurfaceHits = & rg -n 'CultMesh\.query\s*[<(]' Electron/aetheria-rts-generated-bindings.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: generated RTS read surfaces should use CultMesh projection recipes, not raw query wrappers.`n$forbiddenGeneratedQuerySurfaceHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for generated projection recipe checks."
|
||||
}
|
||||
|
||||
$forbiddenGeneratedContextPlumbingHits = & rg -n 'CultMesh\.(queryContextFromVerse|operationContextFromVerse)|\.execute\([^,\n]+,\s*queryContext\(|\.invoke\([^,\n]+,\s*operationContext\(' Electron/aetheria-rts-generated-bindings.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: generated RTS facade should bind surfaces to Verse contexts instead of threading context plumbing.`n$forbiddenGeneratedContextPlumbingHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for generated Verse binding checks."
|
||||
}
|
||||
|
||||
& rg -q 'createAetheriaRuntimeRtsQueryHandles\(' Electron/aetheria-cultmesh.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client is not creating generated query handles."
|
||||
}
|
||||
|
||||
$clientText = Get-Content Electron/aetheria-cultmesh.ts -Raw
|
||||
|
||||
if (-not $clientText.Contains('this.statePointers = createAetheriaRuntimeRtsStatePointers(') -or
|
||||
-not $clientText.Contains('this.queryVerse.context.routeHint,')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client is not creating generated state pointers with its shared Verse route."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('daemonFrame: async () => this.fetchLatestFrameDocument()') -or
|
||||
-not $clientText.Contains('authorityPolicy: async () => this.fetchAuthorityPolicyDocument()')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client state pointers are not resolving through daemon publication readers."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('this.verse = CultMesh.verse("aetheria.local", this.runtimeId)')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client should bind to the shared CultMesh Verse primitive instead of constructing local contexts directly."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('this.queryVerse = this.verse.withRoute("shared-memory"')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client is not deriving a shared-memory Verse view for local projection reads."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('this.commandVerse = this.verse') -or
|
||||
-not $clientText.Contains('.withRoute("network", this.endpoint)') -or
|
||||
-not $clientText.Contains('.withClaim("commander-control"')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client is not deriving a network Verse view with an explicit command authority claim."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('this.publications.statePathDescription')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client projection route metadata does not include the local publication source description."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('return this.queryVerse.queryContext();')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS query call sites should derive query contexts from the shared Verse primitive."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('this.aetheria = createAetheriaRuntimeRtsVerseFacade(')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client should bind generated Aetheria domain sugar to its Verse contexts."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('return this.aetheria.zone().objects.visibleWithin(request);') -or
|
||||
-not $clientText.Contains('return this.aetheria.entity(request.actorEntityKey).pilot.move(') -or
|
||||
-not $clientText.Contains('return this.aetheria.entity(request.actorEntityKey).pilot.target(')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client should route viewport and pilot operations through generated Verse domain sugar."
|
||||
}
|
||||
|
||||
$forbiddenRawContextHits = & rg -n 'CultMesh\.(operationContext|queryContext)\(this\.runtimeId' Electron/aetheria-cultmesh.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client still constructs raw runtime-id contexts instead of using the shared Verse primitive.`n$forbiddenRawContextHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for raw context checks."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('projectionDiagnostics()')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client does not expose generated projection diagnostics."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('describeAetheriaRuntimeRtsQueryHandles(this.queries)')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS projection diagnostics should be read from generated query handle metadata."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('liveFeedDiagnostics()')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client does not expose generated live feed diagnostics."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('describeAetheriaRuntimeRtsLiveFeedSurface(this.createViewportFeed())')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS live feed diagnostics should be read from generated live feed metadata."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('surfaceCatalogDiagnostics()')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client does not expose a unified CultMesh surface catalog diagnostic."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('surfaceCatalogIndexDiagnostics()')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client does not expose a grouped CultMesh surface catalog diagnostic."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('CultMesh.surfaceCatalogIndex(this.surfaceCatalogDiagnostics())')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS grouped surface catalog should use the shared CultMesh catalog index primitive."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('describeAetheriaRuntimeRtsSurfaceCatalog(this.queries, this.operations, this.statePointers)')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS surface catalog should be read from generated query, operation, and state pointer metadata."
|
||||
}
|
||||
|
||||
$forbiddenDirectClientOperationHits = & rg -n 'operations\.set(MoveVector|Target)\.invoke' Electron/aetheria-cultmesh.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client still invokes operation handles directly instead of generated domain sugar.`n$forbiddenDirectClientOperationHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for direct operation handle checks."
|
||||
}
|
||||
|
||||
if (-not $clientText.Contains('CultMesh.liveFeed<AetheriaRuntimeViewportFeedRequest, AetheriaRuntimeViewportFeedSnapshot>')) {
|
||||
Write-Error "Stage 7B verifier failed: RTS viewport feed should use the shared CultMesh live feed primitive."
|
||||
}
|
||||
|
||||
$requiredGeneratedPreloadSymbols = @(
|
||||
'<auto-generated',
|
||||
'channels.mapViewport',
|
||||
'channels.objectsViewport',
|
||||
'channels.gravityViewport',
|
||||
'watchViewportFeed',
|
||||
'channels.viewportFeedUpdate',
|
||||
'channels.viewportFeedStop',
|
||||
'channels.setMoveVector',
|
||||
'channels.setTarget',
|
||||
'channels.surfaceCatalog',
|
||||
'channels.surfaceCatalogIndex'
|
||||
)
|
||||
|
||||
foreach ($symbol in $requiredGeneratedPreloadSymbols) {
|
||||
& rg -F -q $symbol Electron/preload.cjs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: expected generated preload symbol '$symbol' was not found."
|
||||
}
|
||||
}
|
||||
|
||||
$forbiddenMainIpcStringHits = & rg -n 'ipcMain\.handle\("aetheria-rts:' Electron/main.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: Electron main still registers raw RTS IPC channel strings.`n$forbiddenMainIpcStringHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for raw Electron main IPC checks."
|
||||
}
|
||||
|
||||
$forbiddenMainGeneratedHandlerMirrors = @(
|
||||
'AetheriaRtsIpcChannels\.mapViewport',
|
||||
'AetheriaRtsIpcChannels\.objectsViewport',
|
||||
'AetheriaRtsIpcChannels\.setMoveVector'
|
||||
)
|
||||
$forbiddenMainGeneratedHandlerPattern = ($forbiddenMainGeneratedHandlerMirrors -join '|')
|
||||
$forbiddenMainGeneratedHandlerHits = & rg -n $forbiddenMainGeneratedHandlerPattern Electron/main.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: Electron main still mirrors generated IPC handler registration.`n$forbiddenMainGeneratedHandlerHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for generated IPC handler mirror checks."
|
||||
}
|
||||
|
||||
& rg -q 'registerAetheriaRtsIpcHandlers' Electron/main.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: Electron main is not using generated RTS IPC handler registration."
|
||||
}
|
||||
|
||||
$forbiddenClientDirectProjectionReturns = @(
|
||||
'return projectSelectedObjectFromFrame',
|
||||
'return projectInventoryFromFrame',
|
||||
'return projectDaemonHealth',
|
||||
'return projectAuthorityStatus',
|
||||
'return projectStarbridgeSessionSummary'
|
||||
)
|
||||
$forbiddenClientDirectProjectionPattern = ($forbiddenClientDirectProjectionReturns -join '|')
|
||||
$forbiddenClientDirectProjectionHits = & rg -n $forbiddenClientDirectProjectionPattern Electron/aetheria-cultmesh.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client read methods bypass generated query handles.`n$forbiddenClientDirectProjectionHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for direct projection return checks."
|
||||
}
|
||||
|
||||
$requiredRendererContractSymbols = @(
|
||||
'export type AetheriaRtsApi',
|
||||
'export type ViewportResponse',
|
||||
'export type ObjectsViewportResponse',
|
||||
'export type GravityViewportResponse',
|
||||
'export type AetheriaRuntimeViewportFeedRequest',
|
||||
'export type AetheriaRuntimeViewportFeedSnapshot',
|
||||
'watchViewportFeed',
|
||||
'export type AetheriaRuntimeSetMoveVectorRequest',
|
||||
'export type AetheriaRuntimeSetTargetRequest'
|
||||
)
|
||||
|
||||
foreach ($symbol in $requiredRendererContractSymbols) {
|
||||
& rg -q $symbol Client/aetheria-rts-contract.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: expected generated renderer contract symbol '$symbol' was not found."
|
||||
}
|
||||
}
|
||||
|
||||
$forbiddenRendererAppPatterns = @(
|
||||
'^type ViewportResponse =',
|
||||
'^type ObjectsViewportResponse =',
|
||||
'^type GravityViewportResponse =',
|
||||
'^type AetheriaRtsApi =',
|
||||
'^type ViewObject =',
|
||||
'^type InventoryItem ='
|
||||
)
|
||||
|
||||
$forbiddenRendererAppPattern = ($forbiddenRendererAppPatterns -join '|')
|
||||
$forbiddenRendererAppHits = & rg -n $forbiddenRendererAppPattern Client/app.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: renderer still owns duplicated RTS contract types.`n$forbiddenRendererAppHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for duplicated renderer contract checks."
|
||||
}
|
||||
|
||||
$forbiddenRendererPollingHits = & rg -n 'setInterval|objectsViewport\(viewport\)|gravityViewport\(viewport\)' Client/app.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: renderer still owns the RTS viewport polling loop instead of subscribing to the generated feed.`n$forbiddenRendererPollingHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for renderer polling checks."
|
||||
}
|
||||
|
||||
& rg -q 'watchViewportFeed' Client/app.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: renderer is not subscribing to the generated viewport feed."
|
||||
}
|
||||
|
||||
$forbiddenBindingPatterns = @(
|
||||
'decodeViewportDocument',
|
||||
'viewportRecordKey',
|
||||
'aetheriaRuntimeRtsViewportDocumentSlots',
|
||||
'aetheriaRuntimeRtsViewportObjectSlots',
|
||||
'^export type ViewportResponse =',
|
||||
'^export type ObjectsViewportResponse =',
|
||||
'^export type GravityViewportResponse =',
|
||||
'^export type ViewObject =',
|
||||
'^export type InventoryItem =',
|
||||
'^export type SelectedObjectRequest ='
|
||||
)
|
||||
|
||||
$forbiddenBindingPattern = ($forbiddenBindingPatterns -join '|')
|
||||
$forbiddenBindingHits = & rg -n $forbiddenBindingPattern Electron/aetheria-rts-bindings.ts 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Error "Stage 7B verifier failed: stale remote viewport decoder surface still exists in RTS bindings.`n$forbiddenBindingHits"
|
||||
}
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
Write-Error "Stage 7B verifier could not run rg for stale binding checks."
|
||||
}
|
||||
|
||||
& rg -q 'projectViewportFromFrame' Electron/aetheria-rts-local-projection.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: local RTS frame projection entry point was not found."
|
||||
}
|
||||
|
||||
$requiredProjectionSymbols = @(
|
||||
'projectObjectsViewportFromFrame',
|
||||
'projectGravityViewportFromFrame',
|
||||
'AetheriaRtsSchemas.objectsViewport',
|
||||
'AetheriaRtsSchemas.gravityViewport',
|
||||
'projectSelectedObjectFromFrame',
|
||||
'projectInventoryFromFrame',
|
||||
'AetheriaRtsSchemas.selectedObject',
|
||||
'AetheriaRtsSchemas.inventory',
|
||||
'projectDaemonHealth',
|
||||
'projectAuthorityStatus',
|
||||
'projectStarbridgeSessionSummary',
|
||||
'AetheriaRtsSchemas.starbridgeSessionSummary'
|
||||
)
|
||||
|
||||
foreach ($symbol in $requiredProjectionSymbols) {
|
||||
& rg -q $symbol Electron/aetheria-rts-local-projection.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: expected local RTS projection symbol '$symbol' was not found."
|
||||
}
|
||||
}
|
||||
|
||||
& rg -q 'SingleFileMessagePackBackingStore' Electron/aetheria-local-publication-reader.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: local CultCache publication reader was not found."
|
||||
}
|
||||
|
||||
& rg -q 'readStarbridgeSessionSummary' Electron/aetheria-local-publication-reader.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: local Starbridge summary publication reader was not found."
|
||||
}
|
||||
|
||||
& rg -q '.daemon.starbridge.session.cc' Electron/aetheria-local-publication-reader.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: RTS client is not reading the daemon Starbridge sidecar publication."
|
||||
}
|
||||
|
||||
$requiredGeneratedBindingSymbols = @(
|
||||
'aetheriaRuntimeDaemonCommandDocumentSlots',
|
||||
'aetheriaRuntimeDaemonFrameDocumentSlots',
|
||||
'aetheriaRuntimeDaemonHealthDocumentSlots',
|
||||
'aetheriaRuntimeRtsViewportDocumentSlots',
|
||||
'aetheriaRuntimeObjectsViewportDocumentSlots',
|
||||
'aetheriaRuntimeGravityViewportDocumentSlots',
|
||||
'aetheriaRuntimeSelectedObjectDocumentSlots',
|
||||
'aetheriaRuntimeInventoryDocumentSlots',
|
||||
'aetheriaRuntimeStarbridgeScenarioDocumentSlots',
|
||||
'aetheriaRuntimeStarbridgeSessionDocumentSlots',
|
||||
'aetheriaRuntimeStarbridgeSessionSummaryDocumentSlots',
|
||||
'aetheriaRuntimeStarbridgeStationStockItemSlots',
|
||||
'aetheriaRuntimeStarbridgeWaveDefinitionSlots',
|
||||
'aetheriaRuntimeStarbridgeWaveForecastSlots',
|
||||
'aetheriaRuntimeStarbridgeRuntimeRoleSlots',
|
||||
'aetheriaRuntimeStarbridgeBaseStatusSlots',
|
||||
'aetheriaRuntimeRtsViewportBoundsSlots',
|
||||
'aetheriaRuntimeRtsViewportObjectSlots',
|
||||
'aetheriaRuntimeVerseAuthorityPolicyDocumentSlots',
|
||||
'aetheriaRuntimeAuthorityRuleSlots',
|
||||
'aetheriaRuntimeAuthorityLeaseDocumentSlots',
|
||||
'aetheriaRuntimeRunCheckpointCommitSlots',
|
||||
'aetheriaRuntimeZoneSnapshotCommitSlots',
|
||||
'aetheriaRuntimeEntitySnapshotCommitSlots',
|
||||
'aetheriaRuntimeBodySnapshotCommitSlots',
|
||||
'aetheriaRuntimeEntityStatGridCommitSlots',
|
||||
'aetheriaRuntimeCargoBayLoadoutCommitSlots',
|
||||
'aetheriaRuntimeLoadoutItemSlotCommitSlots',
|
||||
'aetheriaRuntimeLoadoutItemCommitSlots',
|
||||
'AetheriaRuntimeDaemonCommandKinds',
|
||||
'AetheriaRtsSchemas'
|
||||
)
|
||||
|
||||
foreach ($symbol in $requiredGeneratedBindingSymbols) {
|
||||
& rg -q $symbol Electron/aetheria-rts-generated-bindings.ts
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7B verifier failed: expected generated RTS binding symbol '$symbol' was not found."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Stage 7B RTS client verifier passed."
|
||||
90
Aetheria.Rts.Web/scripts/verify-stage7c-electron-shell.ps1
Normal file
90
Aetheria.Rts.Web/scripts/verify-stage7c-electron-shell.ps1
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $root
|
||||
|
||||
npm run build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: RTS build failed."
|
||||
}
|
||||
|
||||
if (-not (Get-Content wwwroot\index.html -Raw).Contains('runtime-surface-details')) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: built renderer markup does not expose runtime surface diagnostics."
|
||||
}
|
||||
|
||||
if (-not (Get-Content wwwroot\app.js -Raw).Contains('window.aetheriaRts.surfaceCatalogIndex()')) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: built renderer script does not read the CultMesh surface catalog index."
|
||||
}
|
||||
|
||||
$mainText = Get-Content Electron\main.ts -Raw
|
||||
if (-not $mainText.Contains('await api.surfaceCatalog()') -or -not $mainText.Contains('await api.surfaceCatalogIndex()')) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: Electron smoke does not call the preload CultMesh surface catalog APIs."
|
||||
}
|
||||
|
||||
if (-not $mainText.Contains('runtimeSurfaceDetails.includes("shared-memory")') -or -not $mainText.Contains('daemon:aetheria.frame.latest.v1')) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: Electron smoke does not verify rendered CultMesh route/source metadata."
|
||||
}
|
||||
|
||||
if (-not $mainText.Contains('await api.setMoveVector') -or
|
||||
-not $mainText.Contains('moveReceipt?.commandId') -or
|
||||
-not $mainText.Contains('moveReceipt?.operationId') -or
|
||||
-not $mainText.Contains('moveReceipt?.accepted') -or
|
||||
-not $mainText.Contains('moveReceipt?.route')) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: Electron smoke does not verify typed operation receipts through preload."
|
||||
}
|
||||
|
||||
if (-not $mainText.Contains('AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS') -or
|
||||
-not $mainText.Contains('--peer-cultmesh-endpoint') -or
|
||||
-not $mainText.Contains('AETHERIA_RTS_VERSE_ID') -or
|
||||
-not $mainText.Contains('--verse-id') -or
|
||||
-not $mainText.Contains('AETHERIA_RTS_DAEMON_ID') -or
|
||||
-not $mainText.Contains('--daemon-id')) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: Electron launcher does not pass Verse identity and peer CultMesh endpoints into the local daemon."
|
||||
}
|
||||
|
||||
$electron = Join-Path $root "node_modules\electron\dist\electron.exe"
|
||||
if (!(Test-Path -LiteralPath $electron)) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: electron.exe was not found at $electron."
|
||||
}
|
||||
|
||||
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("aetheria-stage7c-electron-" + [Guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $tempRoot | Out-Null
|
||||
$resultPath = Join-Path $tempRoot "electron-smoke-result.json"
|
||||
$udp = [System.Net.Sockets.UdpClient]::new(0)
|
||||
$port = ([System.Net.IPEndPoint]$udp.Client.LocalEndPoint).Port
|
||||
$udp.Close()
|
||||
|
||||
try {
|
||||
$env:AETHERIA_RTS_ELECTRON_SMOKE = "1"
|
||||
$env:AETHERIA_RTS_RUNTIME_ROOT = $tempRoot
|
||||
$env:AETHERIA_RTS_CULTMESH_PORT = $port.ToString()
|
||||
$env:AETHERIA_RTS_VERSE_ID = "aetheria.stage7c.electron"
|
||||
$env:AETHERIA_RTS_DAEMON_ID = "stage7c-starfire"
|
||||
$env:AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS = "rudp://127.0.0.1:1"
|
||||
$env:AETHERIA_RTS_ELECTRON_SMOKE_RESULT = $resultPath
|
||||
$process = Start-Process -FilePath $electron -ArgumentList "." -WorkingDirectory $root -Wait -PassThru
|
||||
if ($process.ExitCode -ne 0) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: Electron exited with code $($process.ExitCode)."
|
||||
}
|
||||
|
||||
if (!(Test-Path -LiteralPath $resultPath)) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: Electron did not write a smoke result."
|
||||
}
|
||||
|
||||
$result = Get-Content -Raw -LiteralPath $resultPath | ConvertFrom-Json
|
||||
if ($result.ok -ne $true) {
|
||||
Write-Error "Stage 7C Electron shell verifier failed: $($result.error)"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item Env:\AETHERIA_RTS_ELECTRON_SMOKE -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\AETHERIA_RTS_RUNTIME_ROOT -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\AETHERIA_RTS_CULTMESH_PORT -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\AETHERIA_RTS_VERSE_ID -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\AETHERIA_RTS_DAEMON_ID -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\AETHERIA_RTS_PEER_CULTMESH_ENDPOINTS -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\AETHERIA_RTS_ELECTRON_SMOKE_RESULT -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "Stage 7C Electron shell verifier passed."
|
||||
156
Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1
Normal file
156
Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$repoRoot = Split-Path -Parent $root
|
||||
Set-Location $root
|
||||
|
||||
npm run build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7C verifier failed: RTS build failed."
|
||||
}
|
||||
|
||||
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("aetheria-stage7c-" + [Guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $tempRoot | Out-Null
|
||||
$statePath = Join-Path $tempRoot "aetheria-rts.cc"
|
||||
$endpoint = $null
|
||||
|
||||
try {
|
||||
$daemonOutput = dotnet run --project (Join-Path $repoRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj") -- `
|
||||
--state $statePath `
|
||||
--once `
|
||||
--rts-cultmesh-port 0 2>&1
|
||||
$daemonOutput | Out-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7C verifier failed: one-shot daemon did not complete."
|
||||
}
|
||||
|
||||
foreach ($line in $daemonOutput) {
|
||||
if ($line -match "Aetheria RTS CultMesh endpoint: (rudp://127\.0\.0\.1:\d+)") {
|
||||
$endpoint = $Matches[1]
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($endpoint)) {
|
||||
Write-Error "Stage 7C verifier failed: one-shot daemon did not report an RTS CultMesh endpoint."
|
||||
}
|
||||
|
||||
$smokeScript = Join-Path $tempRoot "stage7c-smoke.mjs"
|
||||
@'
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const statePath = process.env.AETHERIA_STAGE7C_STATE_PATH;
|
||||
const endpoint = process.env.AETHERIA_STAGE7C_ENDPOINT;
|
||||
const clientModulePath = process.env.AETHERIA_STAGE7C_CLIENT_MODULE;
|
||||
if (!statePath || !endpoint)
|
||||
throw new Error("Stage 7C verifier missing state path or endpoint.");
|
||||
if (!clientModulePath)
|
||||
throw new Error("Stage 7C verifier missing compiled client module path.");
|
||||
|
||||
const { AetheriaCultMeshClient } = await import(pathToFileURL(clientModulePath).href);
|
||||
|
||||
const client = new AetheriaCultMeshClient(endpoint, statePath, "stage7c-verifier");
|
||||
const viewport = await client.mapViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 });
|
||||
const objectsViewport = await client.objectsViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 });
|
||||
const gravityViewport = await client.gravityViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 });
|
||||
if (!Number.isFinite(viewport.frameId))
|
||||
throw new Error("Viewport projection did not produce a frame id.");
|
||||
if (!Array.isArray(viewport.objects) || viewport.objects.length === 0)
|
||||
throw new Error("Viewport projection did not expose visible objects.");
|
||||
if (objectsViewport.schema !== "gamecult.aetheria.objects_viewport.v1")
|
||||
throw new Error(`Objects viewport used unexpected schema: ${objectsViewport.schema}`);
|
||||
if (gravityViewport.schema !== "gamecult.aetheria.gravity_viewport.v1")
|
||||
throw new Error(`Gravity viewport used unexpected schema: ${gravityViewport.schema}`);
|
||||
if (!Array.isArray(objectsViewport.objects) || objectsViewport.objects.length !== viewport.objects.length)
|
||||
throw new Error("Objects viewport did not match composed map object set.");
|
||||
if (!Array.isArray(gravityViewport.gravityInfluences) || gravityViewport.gravityInfluences.length !== viewport.gravityInfluences.length)
|
||||
throw new Error("Gravity viewport did not match composed map gravity set.");
|
||||
|
||||
const selectedIndex = viewport.controlledEntityIndices[0] ?? viewport.objects[0]?.entityIndex;
|
||||
if (!Number.isFinite(selectedIndex))
|
||||
throw new Error("No entity was available for selected-object projection.");
|
||||
|
||||
const selected = await client.selectedObject({ entityIndex: selectedIndex });
|
||||
if (!selected.selected)
|
||||
throw new Error(`Selected-object projection did not resolve entity ${selectedIndex}.`);
|
||||
|
||||
const inventory = await client.inventory({ entityIndex: selectedIndex });
|
||||
if (!Array.isArray(inventory.equipment) || !Array.isArray(inventory.cargo))
|
||||
throw new Error("Inventory projection did not return equipment and cargo arrays.");
|
||||
|
||||
const health = await client.daemonHealth();
|
||||
if (health.status !== "healthy")
|
||||
throw new Error(`Daemon health projection was not healthy: ${health.status}`);
|
||||
|
||||
const authority = await client.authorityStatus();
|
||||
if (authority.policyId !== "aetheria.trusted-coop.v1")
|
||||
throw new Error(`Unexpected authority policy id: ${authority.policyId}`);
|
||||
|
||||
const starbridge = await client.starbridgeSession();
|
||||
if (starbridge.schema !== "gamecult.aetheria.starbridge_session_summary.v1")
|
||||
throw new Error(`Unexpected Starbridge schema: ${starbridge.schema}`);
|
||||
if (starbridge.scenarioName !== "Frontier Fabricator Defense")
|
||||
throw new Error(`Unexpected Starbridge scenario: ${starbridge.scenarioName}`);
|
||||
if (!Array.isArray(starbridge.stationStock) || !starbridge.stationStock.some(item => item.itemKey === "repair-parts"))
|
||||
throw new Error("Starbridge session did not expose daemon-seeded station stock.");
|
||||
if (!Array.isArray(starbridge.waveForecast) || !starbridge.waveForecast.some(wave => wave.displayName === "Scout Probe"))
|
||||
throw new Error("Starbridge session did not expose daemon-seeded wave forecast.");
|
||||
|
||||
const surfaceCatalog = client.surfaceCatalogDiagnostics();
|
||||
if (surfaceCatalog.catalogId !== "gamecult.aetheria.rts.surfaces.v1")
|
||||
throw new Error(`Unexpected RTS surface catalog id: ${surfaceCatalog.catalogId}`);
|
||||
if (!surfaceCatalog.surfaces.some(surface => surface.surfaceId === "gamecult.aetheria.rts.viewport_feed.v1" && surface.kind === "live-feed"))
|
||||
throw new Error("RTS surface catalog did not expose the viewport live feed.");
|
||||
if (!surfaceCatalog.surfaces.some(surface => surface.surfaceId === "gamecult.aetheria.pilot.set_move_vector.v1" && surface.kind === "operation"))
|
||||
throw new Error("RTS surface catalog did not expose the move operation.");
|
||||
if (!surfaceCatalog.surfaces.some(surface =>
|
||||
surface.surfaceId === "daemon:aetheria.frame.latest.v1" &&
|
||||
surface.kind === "state-pointer" &&
|
||||
surface.routeHint?.kind === "shared-memory" &&
|
||||
surface.sources?.some(source => source.schemaId === "gamecult.aetheria.daemon_frame.v1")))
|
||||
throw new Error("RTS surface catalog did not expose the daemon frame state pointer with shared route/source metadata.");
|
||||
|
||||
const surfaceIndex = client.surfaceCatalogIndexDiagnostics();
|
||||
if (surfaceIndex.catalogId !== surfaceCatalog.catalogId)
|
||||
throw new Error("RTS surface catalog index did not preserve the catalog id.");
|
||||
if (!Array.isArray(surfaceIndex.operations) || surfaceIndex.operations.length < 2)
|
||||
throw new Error("RTS surface catalog index did not group operation surfaces.");
|
||||
if (!Array.isArray(surfaceIndex.liveFeeds) || !surfaceIndex.liveFeeds.some(surface => surface.surfaceId === "gamecult.aetheria.rts.viewport_feed.v1"))
|
||||
throw new Error("RTS surface catalog index did not group the viewport live feed.");
|
||||
if (!Array.isArray(surfaceIndex.queries) || !surfaceIndex.queries.some(surface => surface.surfaceId === "gamecult.aetheria.objects_viewport.v1"))
|
||||
throw new Error("RTS surface catalog index did not group generated query surfaces.");
|
||||
if (!Array.isArray(surfaceIndex.statePointers) || !surfaceIndex.statePointers.some(surface => surface.surfaceId === "daemon:aetheria.frame.latest.v1"))
|
||||
throw new Error("RTS surface catalog index did not group generated state pointer surfaces.");
|
||||
|
||||
console.log(JSON.stringify({
|
||||
frameId: viewport.frameId,
|
||||
visibleObjects: viewport.objects.length,
|
||||
gravityInfluences: gravityViewport.gravityInfluences.length,
|
||||
selectedEntityIndex: selected.selected.entityIndex,
|
||||
equipmentItems: inventory.equipment.length,
|
||||
cargoItems: inventory.cargo.length,
|
||||
health: health.status,
|
||||
policyId: authority.policyId,
|
||||
starbridgeScenario: starbridge.scenarioName,
|
||||
starbridgeStock: starbridge.stationStock.length,
|
||||
surfaceCatalog: {
|
||||
surfaces: surfaceCatalog.surfaces.length,
|
||||
operations: surfaceIndex.operations.length,
|
||||
queries: surfaceIndex.queries.length,
|
||||
liveFeeds: surfaceIndex.liveFeeds.length,
|
||||
statePointers: surfaceIndex.statePointers.length
|
||||
}
|
||||
}, null, 2));
|
||||
'@ | Set-Content -LiteralPath $smokeScript -Encoding UTF8
|
||||
|
||||
$env:AETHERIA_STAGE7C_STATE_PATH = $statePath
|
||||
$env:AETHERIA_STAGE7C_ENDPOINT = $endpoint
|
||||
$env:AETHERIA_STAGE7C_CLIENT_MODULE = Join-Path $root "electron-dist\aetheria-cultmesh.js"
|
||||
node $smokeScript
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7C verifier failed: local Electron runtime facade could not read daemon publications."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "Stage 7C local runtime verifier passed."
|
||||
139
Aetheria.Rts.Web/scripts/verify-stage7d-remote-cultmesh.ps1
Normal file
139
Aetheria.Rts.Web/scripts/verify-stage7d-remote-cultmesh.ps1
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$repoRoot = Split-Path -Parent $root
|
||||
Set-Location $root
|
||||
|
||||
npm run build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7D remote CultMesh verifier failed: RTS build failed."
|
||||
}
|
||||
|
||||
$daemonProject = Join-Path $repoRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj"
|
||||
dotnet build $daemonProject -v:minimal
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7D remote CultMesh verifier failed: daemon build failed."
|
||||
}
|
||||
|
||||
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("aetheria-stage7d-remote-" + [Guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $tempRoot | Out-Null
|
||||
$statePath = Join-Path $tempRoot "aetheria-raven.cc"
|
||||
$daemonOut = Join-Path $tempRoot "daemon.out.log"
|
||||
$daemonErr = Join-Path $tempRoot "daemon.err.log"
|
||||
$port = Get-Random -Minimum 41000 -Maximum 64000
|
||||
$endpoint = "rudp://127.0.0.1:$port"
|
||||
$daemon = $null
|
||||
|
||||
try {
|
||||
$daemon = Start-Process -FilePath "dotnet" -ArgumentList @(
|
||||
"run",
|
||||
"--no-build",
|
||||
"--project",
|
||||
$daemonProject,
|
||||
"--",
|
||||
"--state",
|
||||
$statePath,
|
||||
"--rts-cultmesh-host",
|
||||
"127.0.0.1",
|
||||
"--rts-cultmesh-port",
|
||||
$port,
|
||||
"--tick-interval-ms",
|
||||
"20",
|
||||
"--fixed-delta-ms",
|
||||
"20",
|
||||
"--api-publication-interval-ms",
|
||||
"100000"
|
||||
) -RedirectStandardOutput $daemonOut -RedirectStandardError $daemonErr -WindowStyle Hidden -PassThru
|
||||
|
||||
$deadline = (Get-Date).AddSeconds(40)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if ($daemon.HasExited) {
|
||||
Write-Error "Stage 7D remote CultMesh verifier failed: daemon exited early ($($daemon.ExitCode)).`n$(Get-Content $daemonErr -Raw)`n$(Get-Content $daemonOut -Raw)"
|
||||
}
|
||||
|
||||
if ((Test-Path $daemonOut) -and ((Get-Content $daemonOut -Raw) -match "Aetheria Verse daemon published frame")) {
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
|
||||
if (-not (Test-Path $daemonOut) -or -not ((Get-Content $daemonOut -Raw) -match "Aetheria Verse daemon published frame")) {
|
||||
Write-Error "Stage 7D remote CultMesh verifier failed: daemon did not publish a frame.`n$(if (Test-Path $daemonOut) { Get-Content $daemonOut -Raw })"
|
||||
}
|
||||
|
||||
$smokeScript = Join-Path $tempRoot "remote-smoke.mjs"
|
||||
@'
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const endpoint = process.env.AETHERIA_STAGE7D_REMOTE_ENDPOINT;
|
||||
const clientModulePath = process.env.AETHERIA_STAGE7D_REMOTE_CLIENT_MODULE;
|
||||
if (!endpoint)
|
||||
throw new Error("Stage 7D verifier missing remote endpoint.");
|
||||
if (!clientModulePath)
|
||||
throw new Error("Stage 7D verifier missing compiled client module path.");
|
||||
|
||||
const { AetheriaCultMeshClient } = await import(pathToFileURL(clientModulePath).href);
|
||||
const client = new AetheriaCultMeshClient(endpoint, "unused-local-state.cc", "stage7d-starfire", {
|
||||
publicationMode: "remote",
|
||||
snapshotTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
try {
|
||||
await client.waitForFrame(15000);
|
||||
const viewport = await client.mapViewport({ minX: -5000, minY: -5000, maxX: 5000, maxY: 5000 });
|
||||
const actor = viewport.objects.find(object => object.controlled);
|
||||
if (!actor)
|
||||
throw new Error("Remote CultMesh viewport did not expose a controlled unit.");
|
||||
|
||||
const health = await client.daemonHealth();
|
||||
const authority = await client.authorityStatus();
|
||||
const starbridge = await client.starbridgeSession();
|
||||
const receipt = await client.setMoveVector({
|
||||
actorEntityKey: actor.entityKey,
|
||||
directionX: 1,
|
||||
directionY: 0,
|
||||
scalar: 0.1,
|
||||
observedFrameId: viewport.frameId,
|
||||
});
|
||||
|
||||
if (health.status !== "healthy")
|
||||
throw new Error(`Unexpected daemon health ${health.status}.`);
|
||||
if (authority.policyId !== "aetheria.trusted-coop.v1")
|
||||
throw new Error(`Unexpected authority policy ${authority.policyId}.`);
|
||||
if (starbridge.scenarioName !== "Frontier Fabricator Defense")
|
||||
throw new Error(`Unexpected Starbridge scenario ${starbridge.scenarioName}.`);
|
||||
if (!receipt.accepted)
|
||||
throw new Error(`Remote setMoveVector was rejected: ${receipt.diagnostic ?? "no diagnostic"}`);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
endpoint,
|
||||
frameId: viewport.frameId,
|
||||
visibleObjects: viewport.objects.length,
|
||||
health: health.status,
|
||||
policyId: authority.policyId,
|
||||
starbridgeScenario: starbridge.scenarioName,
|
||||
operationId: receipt.operationId,
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
'@ | Set-Content -LiteralPath $smokeScript -Encoding UTF8
|
||||
|
||||
$env:AETHERIA_STAGE7D_REMOTE_ENDPOINT = $endpoint
|
||||
$env:AETHERIA_STAGE7D_REMOTE_CLIENT_MODULE = Join-Path $root "electron-dist\aetheria-cultmesh.js"
|
||||
node $smokeScript
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Stage 7D remote CultMesh verifier failed: remote RTS client could not read or command the daemon over CultMesh."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item Env:\AETHERIA_STAGE7D_REMOTE_ENDPOINT -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\AETHERIA_STAGE7D_REMOTE_CLIENT_MODULE -ErrorAction SilentlyContinue
|
||||
if ($daemon -and -not $daemon.HasExited) {
|
||||
Stop-Process -Id $daemon.Id -Force
|
||||
}
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "Stage 7D remote CultMesh verifier passed."
|
||||
17
Aetheria.Rts.Web/tsconfig.electron.json
Normal file
17
Aetheria.Rts.Web/tsconfig.electron.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"noEmitOnError": true,
|
||||
"outDir": "electron-dist",
|
||||
"rootDir": "Electron",
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"Electron/**/*.ts"
|
||||
]
|
||||
}
|
||||
14
Aetheria.Rts.Web/tsconfig.json
Normal file
14
Aetheria.Rts.Web/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"noEmitOnError": true,
|
||||
"outDir": "wwwroot",
|
||||
"rootDir": "Client"
|
||||
},
|
||||
"include": [
|
||||
"Client/**/*.ts"
|
||||
]
|
||||
}
|
||||
51
Aetheria.Rts.Web/wwwroot/index.html
Normal file
51
Aetheria.Rts.Web/wwwroot/index.html
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Aetheria RTS</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="map-pane">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<strong>Aetheria RTS</strong>
|
||||
<span id="status">Connecting</span>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button id="zoom-out" type="button" title="Zoom out">-</button>
|
||||
<output id="zoom-label">1x</output>
|
||||
<button id="zoom-in" type="button" title="Zoom in">+</button>
|
||||
<button id="refresh" type="button" title="Refresh viewport">Refresh</button>
|
||||
</div>
|
||||
</header>
|
||||
<canvas id="map"></canvas>
|
||||
</section>
|
||||
<aside class="side-pane">
|
||||
<section>
|
||||
<h1 id="selection-title">No Selection</h1>
|
||||
<dl id="selection-details"></dl>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Controlled</h2>
|
||||
<div id="controlled-list"></div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Starbridge</h2>
|
||||
<dl id="starbridge-details"></dl>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Frame</h2>
|
||||
<dl id="frame-details"></dl>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Runtime Surfaces</h2>
|
||||
<dl id="runtime-surface-details"></dl>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
172
Aetheria.Rts.Web/wwwroot/styles.css
Normal file
172
Aetheria.Rts.Web/wwwroot/styles.css
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #111417;
|
||||
color: #e8edf0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
min-width: 36px;
|
||||
height: 32px;
|
||||
border: 1px solid #41505a;
|
||||
border-radius: 6px;
|
||||
background: #20282d;
|
||||
color: #eef6f8;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2a353b;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.map-pane {
|
||||
display: grid;
|
||||
grid-template-rows: 52px minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: #0f1215;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #263038;
|
||||
background: #171d21;
|
||||
}
|
||||
|
||||
.topbar span {
|
||||
display: inline-block;
|
||||
margin-left: 12px;
|
||||
color: #9fb0b9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#zoom-label {
|
||||
min-width: 52px;
|
||||
color: #c5d2d7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#map {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0d1013;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.side-pane {
|
||||
display: grid;
|
||||
grid-auto-rows: min-content;
|
||||
gap: 18px;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 18px;
|
||||
border-left: 1px solid #263038;
|
||||
background: #171d21;
|
||||
}
|
||||
|
||||
.side-pane section {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
grid-template-columns: 96px minmax(0, 1fr);
|
||||
gap: 7px 10px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: #91a4ad;
|
||||
}
|
||||
|
||||
dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#controlled-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.controlled-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px solid #263038;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #91a4ad;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: minmax(0, 1fr) 230px;
|
||||
}
|
||||
|
||||
.side-pane {
|
||||
border-top: 1px solid #263038;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.map-pane {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
1797
Aetheria.State.AuthoritySmoke/Program.cs
Normal file
1797
Aetheria.State.AuthoritySmoke/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
15
Aetheria.State.Freeze/Aetheria.State.Freeze.csproj
Normal file
15
Aetheria.State.Freeze/Aetheria.State.Freeze.csproj
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
|
||||
<ProjectReference Include="..\..\CultLib\src\GameCult.Geometry\GameCult.Geometry.csproj" />
|
||||
<Compile Include="..\Assets\Scripts\ServerShared\YmirPhysicsContracts.cs" Link="YmirPhysicsContracts.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
1356
Aetheria.State.Freeze/Program.cs
Normal file
1356
Aetheria.State.Freeze/Program.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,5 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aetheria.State\Aetheria.State.csproj" />
|
||||
<ProjectReference Include="..\Aetheria.State.Unity\Aetheria.State.Unity.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Aetheria.State;
|
||||
using GameCult.Aetheria.State.Unity;
|
||||
|
||||
return await ProgramMainAsync(args).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -16,6 +15,7 @@ static async Task<int> ProgramMainAsync(string[] args)
|
|||
var endpoint = RequireOption(options, "endpoint");
|
||||
var runtimeId = ReadOption(options, "runtime-id", "aetheria-verse-replica");
|
||||
var replicaPath = ResolveReplicaPath(options);
|
||||
SeedBaselineReplica(options, replicaPath);
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
|
|
@ -59,9 +59,40 @@ static string ResolveReplicaPath(IReadOnlyDictionary<string, string> options)
|
|||
|
||||
var gameDataRoot = RequireOption(options, "game-data-root");
|
||||
var verseId = RequireOption(options, "verse-id");
|
||||
return AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(
|
||||
new DirectoryInfo(Path.GetFullPath(gameDataRoot)),
|
||||
verseId);
|
||||
return Path.Combine(
|
||||
Path.GetFullPath(gameDataRoot),
|
||||
"Verses",
|
||||
$"{SanitizeVerseId(verseId)}.cc");
|
||||
}
|
||||
|
||||
static void SeedBaselineReplica(IReadOnlyDictionary<string, string> options, string replicaPath)
|
||||
{
|
||||
var baselinePath = ReadOption(options, "baseline-state", "");
|
||||
if (string.IsNullOrWhiteSpace(baselinePath) ||
|
||||
File.Exists(replicaPath) ||
|
||||
!File.Exists(baselinePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(replicaPath) ?? ".");
|
||||
File.Copy(baselinePath, replicaPath, overwrite: false);
|
||||
}
|
||||
|
||||
static string SanitizeVerseId(string verseId)
|
||||
{
|
||||
var safeVerseId = string.IsNullOrWhiteSpace(verseId)
|
||||
? "unknown-verse"
|
||||
: new string((verseId ?? "")
|
||||
.Select(ch => char.IsLetterOrDigit(ch) || ch == '-' || ch == '_' || ch == '.'
|
||||
? ch
|
||||
: '-')
|
||||
.ToArray())
|
||||
.Trim('-');
|
||||
|
||||
return string.IsNullOrWhiteSpace(safeVerseId)
|
||||
? "unknown-verse"
|
||||
: safeVerseId;
|
||||
}
|
||||
|
||||
static Dictionary<string, string> ParseOptions(IEnumerable<string> args)
|
||||
|
|
@ -114,7 +145,7 @@ static double ReadDoubleOption(IReadOnlyDictionary<string, string> options, stri
|
|||
static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("Usage:");
|
||||
Console.WriteLine(" sync --endpoint cultnet://host:3075 --replica path-to-replica.cc");
|
||||
Console.WriteLine(" sync --endpoint cultnet://host:3075 --game-data-root GameData --verse-id verse.id");
|
||||
Console.WriteLine(" follow --endpoint cultnet://host:3075 --replica path-to-replica.cc [--poll-seconds 1]");
|
||||
Console.WriteLine(" sync --endpoint cultnet://host:3075 --replica path-to-replica.cc [--runtime-id aetheria-verse-replica]");
|
||||
Console.WriteLine(" sync --endpoint cultnet://host:3075 --game-data-root GameData --verse-id verse.id [--runtime-id aetheria-verse-replica]");
|
||||
Console.WriteLine(" follow --endpoint cultnet://host:3075 --replica path-to-replica.cc [--poll-seconds 1] [--runtime-id aetheria-verse-replica]");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,25 +139,24 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
|
|||
await node.PutDaemonProviderAdvertisementAsync(daemonProvider);
|
||||
await node.PutDaemonHealthAsync(daemonHealth);
|
||||
await node.PutDaemonCommandBoundaryAsync(daemonCommandBoundary);
|
||||
using var daemonCommandClient = await AetheriaRuntimeVerseClient.OpenAsync(
|
||||
using var daemonCommandClient = await AetheriaClient.OpenAsync(
|
||||
statePath,
|
||||
"aetheria-state-smoke-command-client",
|
||||
"smoke-session",
|
||||
startServer: false,
|
||||
pullOnOpen: true);
|
||||
var daemonSubmitEnvelope = await daemonCommandClient.SubmitDaemonCommandAsync(
|
||||
AetheriaRuntimeDaemonCommandDocument.Create(
|
||||
AetheriaRuntimeDaemonCommandKinds.SensorPing,
|
||||
"aetheria-state-smoke",
|
||||
"smoke-session",
|
||||
daemonFrame.FrameId,
|
||||
entityKey.ToString()));
|
||||
daemonCommandClient.Control.SensorPing();
|
||||
|
||||
await using (var commandVerifyNode = await AetheriaStateNode.OpenAsync(
|
||||
statePath,
|
||||
"aetheria-state-smoke-command-check"))
|
||||
{
|
||||
if (commandVerifyNode.ReadObservedDaemonCommands().All(command => command.CommandId != daemonSubmitEnvelope!.CommandId))
|
||||
throw new InvalidOperationException("Verse client daemon submission did not appear as a typed state record.");
|
||||
if (commandVerifyNode.ReadObservedDaemonCommands().All(command =>
|
||||
command.Kind != AetheriaRuntimeDaemonCommandKinds.SensorPing ||
|
||||
command.ClientId != "aetheria-state-smoke-command-client"))
|
||||
{
|
||||
throw new InvalidOperationException("AetheriaClient control submission did not appear as a typed daemon state record.");
|
||||
}
|
||||
}
|
||||
await node.PutDaemonFrameAsync(daemonFrame);
|
||||
await node.PutDaemonGameSurfaceAsync(AetheriaRuntimeEveSurfaceStateProjector.ToState(daemonGameSurface));
|
||||
|
|
@ -480,14 +479,6 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
|
|||
GenerationSeed = 424242,
|
||||
DiscoveredZoneIndices = [0],
|
||||
ZoneKeys = [zoneKey.ToString()],
|
||||
ActionBarBindings =
|
||||
[
|
||||
new AetheriaActionBarBinding
|
||||
{
|
||||
Kind = "weapon-group",
|
||||
WeaponGroup = 0
|
||||
}
|
||||
],
|
||||
UpdatedAtUtc = now
|
||||
});
|
||||
|
||||
|
|
@ -534,23 +525,31 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
|
|||
await node.GetPlayerSettingsAsync(),
|
||||
now));
|
||||
|
||||
using var eveCommandClient = await AetheriaRuntimeVerseClient.OpenAsync(
|
||||
using var eveCommandClient = await AetheriaClient.OpenAsync(
|
||||
statePath,
|
||||
"aetheria-state-smoke-eve-command-client",
|
||||
startServer: false,
|
||||
pullOnOpen: true);
|
||||
var eveSubmitEnvelope = await eveCommandClient.SubmitEveCommandAsync(
|
||||
AetheriaRuntimeEveCommandClient.ToDocument(
|
||||
AetheriaRuntimeEveCommands.SubmitCatalogCommand(
|
||||
statePath,
|
||||
AetheriaRuntimeEveCommandKind.CatalogRefresh,
|
||||
"aetheria-state-smoke")));
|
||||
await eveCommandClient.Ui.InputSettingsAsync(
|
||||
AetheriaRuntimeEveCommandKind.SetBindingOverride,
|
||||
new AetheriaRuntimeInputSettingsCommandBody
|
||||
{
|
||||
ActionName = "Thrust",
|
||||
BindingIndex = 0,
|
||||
InputSystemPath = "<Keyboard>/w",
|
||||
Enabled = true
|
||||
},
|
||||
"aetheria-state-smoke-eve-command-client");
|
||||
await using (var commandVerifyNode = await AetheriaStateNode.OpenAsync(
|
||||
statePath,
|
||||
"aetheria-state-smoke-eve-command-check"))
|
||||
{
|
||||
if (commandVerifyNode.ReadObservedEveCommands().All(command => command.CommandId != eveSubmitEnvelope!.CommandId))
|
||||
throw new InvalidOperationException("Verse client Eve submission did not appear as a typed state record.");
|
||||
if (commandVerifyNode.ReadObservedEveCommands().All(command =>
|
||||
command.Kind != AetheriaRuntimeEveCommandKind.SetBindingOverride ||
|
||||
command.ClientId != "aetheria-state-smoke-eve-command-client"))
|
||||
{
|
||||
throw new InvalidOperationException("AetheriaClient UI submission did not appear as a typed Eve state record.");
|
||||
}
|
||||
}
|
||||
await node.SubmitEveCommandAsync(AetheriaRuntimeEveCommandClient.ToDocument(
|
||||
AetheriaRuntimeEveCommands.SubmitCatalogCommand(
|
||||
|
|
@ -766,8 +765,7 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
|
|||
if (runState?.RunId != "smoke" ||
|
||||
runState.GenerationSeed != 424242 ||
|
||||
runState.CurrentEntityKey != entityKey.ToString() ||
|
||||
runState.ZoneKeys.Length != 1 ||
|
||||
runState.ActionBarBindings.Length != 1)
|
||||
runState.ZoneKeys.Length != 1)
|
||||
{
|
||||
throw new InvalidOperationException("Run state did not survive flush/reopen.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ internal static class AetheriaRuntimeCatalogSnapshotMapper
|
|||
return new AetheriaRuntimeCatalogSnapshot(
|
||||
catalog.Items.Select(FromState).ToArray(),
|
||||
catalog.Corporations.Select(FromState).ToArray(),
|
||||
catalog.NameFiles.Select(FromState).ToArray());
|
||||
catalog.NameFiles.Select(FromState).ToArray(),
|
||||
AetheriaRuntimeStateMapper.ToRuntimeTradeValueSettings(catalog.TradeValuePolicy));
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCatalogItem FromState(AetheriaItemDefinition item)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -13,6 +13,7 @@
|
|||
<ProjectReference Include="..\..\CultLib\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
|
||||
<ProjectReference Include="..\..\CultLib\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||
<ProjectReference Include="..\..\CultLib\src\GameCult.Networking\GameCult.Networking.csproj" />
|
||||
<ProjectReference Include="..\..\CultMath\src\CultMath\CultMath.csproj" />
|
||||
<ProjectReference Include="..\GameCult.Eve.Surface.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ public sealed class AetheriaCatalogSnapshot
|
|||
public AetheriaCatalogSnapshot(
|
||||
IEnumerable<AetheriaItemDefinition> items,
|
||||
IEnumerable<AetheriaCorporation> corporations,
|
||||
IEnumerable<AetheriaNameFile> nameFiles)
|
||||
IEnumerable<AetheriaNameFile> nameFiles,
|
||||
AetheriaTradeValuePolicy? tradeValuePolicy = null)
|
||||
{
|
||||
Items = items.OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
Corporations = corporations.OrderBy(corporation => corporation.Name, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
NameFiles = nameFiles.OrderBy(nameFile => nameFile.Name, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
TradeValuePolicy = tradeValuePolicy;
|
||||
|
||||
_itemsByLegacyId = Items
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item.LegacyId))
|
||||
|
|
@ -45,6 +47,8 @@ public sealed class AetheriaCatalogSnapshot
|
|||
|
||||
public IReadOnlyList<AetheriaNameFile> NameFiles { get; }
|
||||
|
||||
public AetheriaTradeValuePolicy? TradeValuePolicy { get; }
|
||||
|
||||
public IEnumerable<AetheriaItemDefinition> TradeItems =>
|
||||
Items.Where(item => item.Price > 0).OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ public static class AetheriaCatalogSurfaceProjector
|
|||
("names", catalog.GetNameFile(corporation)?.Name ?? ""),
|
||||
("influence", corporation.InfluenceDistance.ToString())))
|
||||
.ToArray();
|
||||
var refreshCommand = GameCult.Mesh.CultMesh.OperationBindingRecord(
|
||||
GameCult.Mesh.CultMesh.OperationBinding(
|
||||
AetheriaRuntimeCatalogCommands.Refresh,
|
||||
label: "Refresh",
|
||||
routeHint: new GameCult.Mesh.CultMeshRouteHint(
|
||||
GameCult.Mesh.CultMeshLocalityKind.Automatic,
|
||||
"cultmesh")));
|
||||
|
||||
return new EveSurfaceState
|
||||
{
|
||||
|
|
@ -69,9 +76,12 @@ public static class AetheriaCatalogSurfaceProjector
|
|||
[
|
||||
new EveCommandTemplate
|
||||
{
|
||||
Command = AetheriaRuntimeCatalogCommands.Refresh,
|
||||
Label = "Refresh",
|
||||
Transport = "cultmesh"
|
||||
Command = refreshCommand.OperationId,
|
||||
Label = refreshCommand.Label,
|
||||
Transport = refreshCommand.RouteDescription,
|
||||
SchemaId = refreshCommand.SchemaId,
|
||||
RouteKind = refreshCommand.RouteKind,
|
||||
RouteDescription = refreshCommand.RouteDescription
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ public static class AetheriaDocumentRegistry
|
|||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeSession>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaMigrationLedger>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaLegacyCatalogQuarantine>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaTradeValuePolicy>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaPlayerSettings>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaLoadoutTemplate>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRunState>(registry),
|
||||
|
|
@ -44,8 +45,27 @@ public static class AetheriaDocumentRegistry
|
|||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonHealthDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonCommandBoundaryDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonFrameDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeRtsViewportDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeObjectsViewportDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeGravityViewportDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeCurrentZoneDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeCurrentEntityDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeCurrentDockingDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeZoneContactsDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeStationRefitDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeSectorMapDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeZoneDetailsDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeZoneRenderDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeSelectedObjectDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeInventoryDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeStarbridgeScenarioDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeStarbridgeSessionDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeStarbridgeSessionSummaryDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonSoaViewDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeVerseAuthorityPolicyDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeAuthorityLeaseDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeDaemonCommandDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeCommittedCommandFactDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<AetheriaRuntimeEveCommandDocument>(registry),
|
||||
CultNetDocumentBinding.ForDocument<EveSurfaceState>(registry),
|
||||
CultNetDocumentBinding.ForDocument<EveProviderAdvertisementState>(registry)
|
||||
|
|
@ -62,6 +82,7 @@ public static class AetheriaDocumentRegistry
|
|||
typeof(AetheriaRuntimeSession),
|
||||
typeof(AetheriaMigrationLedger),
|
||||
typeof(AetheriaLegacyCatalogQuarantine),
|
||||
typeof(AetheriaTradeValuePolicy),
|
||||
typeof(AetheriaPlayerSettings),
|
||||
typeof(AetheriaLoadoutTemplate),
|
||||
typeof(AetheriaRunState),
|
||||
|
|
@ -73,8 +94,27 @@ public static class AetheriaDocumentRegistry
|
|||
typeof(AetheriaRuntimeDaemonHealthDocument),
|
||||
typeof(AetheriaRuntimeDaemonCommandBoundaryDocument),
|
||||
typeof(AetheriaRuntimeDaemonFrameDocument),
|
||||
typeof(AetheriaRuntimeRtsViewportDocument),
|
||||
typeof(AetheriaRuntimeObjectsViewportDocument),
|
||||
typeof(AetheriaRuntimeGravityViewportDocument),
|
||||
typeof(AetheriaRuntimeCurrentZoneDocument),
|
||||
typeof(AetheriaRuntimeCurrentEntityDocument),
|
||||
typeof(AetheriaRuntimeCurrentDockingDocument),
|
||||
typeof(AetheriaRuntimeZoneContactsDocument),
|
||||
typeof(AetheriaRuntimeStationRefitDocument),
|
||||
typeof(AetheriaRuntimeSectorMapDocument),
|
||||
typeof(AetheriaRuntimeZoneDetailsDocument),
|
||||
typeof(AetheriaRuntimeZoneRenderDocument),
|
||||
typeof(AetheriaRuntimeSelectedObjectDocument),
|
||||
typeof(AetheriaRuntimeInventoryDocument),
|
||||
typeof(AetheriaRuntimeStarbridgeScenarioDocument),
|
||||
typeof(AetheriaRuntimeStarbridgeSessionDocument),
|
||||
typeof(AetheriaRuntimeStarbridgeSessionSummaryDocument),
|
||||
typeof(AetheriaRuntimeDaemonSoaViewDocument),
|
||||
typeof(AetheriaRuntimeVerseAuthorityPolicyDocument),
|
||||
typeof(AetheriaRuntimeAuthorityLeaseDocument),
|
||||
typeof(AetheriaRuntimeDaemonCommandDocument),
|
||||
typeof(AetheriaRuntimeCommittedCommandFactDocument),
|
||||
typeof(AetheriaRuntimeEveCommandDocument),
|
||||
typeof(EveSurfaceState),
|
||||
typeof(EveProviderAdvertisementState)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public sealed class AetheriaEveCommandAcceptanceReport
|
|||
public int AcceptedInputSettingsCommands { get; set; }
|
||||
public int AcceptedLoadoutTemplateCommands { get; set; }
|
||||
public int AcceptedVerseHostCommands { get; set; }
|
||||
public int AcceptedTradeValuePolicyCommands { get; set; }
|
||||
public int RejectedCommands { get; set; }
|
||||
public string[] AcceptedCommandIds { get; set; } = [];
|
||||
public string[] RejectedCommandIds { get; set; } = [];
|
||||
|
|
@ -88,6 +89,14 @@ public static class AetheriaEveCommandBridge
|
|||
await ExecuteVerseHostCommandAsync(node, command).ConfigureAwait(false);
|
||||
report.AcceptedVerseHostCommands++;
|
||||
break;
|
||||
case AetheriaRuntimeEveCommandKind.TradeValuePolicyRefresh:
|
||||
case AetheriaRuntimeEveCommandKind.SetTradeValueQualityMinimum:
|
||||
case AetheriaRuntimeEveCommandKind.SetTradeValueQualityMaximum:
|
||||
case AetheriaRuntimeEveCommandKind.SetTradeValueQualityExponent:
|
||||
case AetheriaRuntimeEveCommandKind.SetTradeValueTierQuality:
|
||||
await ExecuteTradeValuePolicyCommandAsync(node, command).ConfigureAwait(false);
|
||||
report.AcceptedTradeValuePolicyCommands++;
|
||||
break;
|
||||
}
|
||||
|
||||
accepted.Add(command.CommandId ?? "");
|
||||
|
|
@ -283,6 +292,50 @@ public static class AetheriaEveCommandBridge
|
|||
loadout).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task ExecuteTradeValuePolicyCommandAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaRuntimeEveCommandDocument command)
|
||||
{
|
||||
var policy = await node.GetTradeValuePolicyAsync().ConfigureAwait(false) ??
|
||||
AetheriaRuntimeStateMapper.ToTradeValuePolicy(
|
||||
AetheriaRuntimeTradeValueSettings.Default,
|
||||
command.IssuedAtUtc);
|
||||
policy.QualityPriceModifier ??= new AetheriaExponentialLerp();
|
||||
policy.Tiers ??= Array.Empty<AetheriaItemRarityTier>();
|
||||
var persistPolicy = false;
|
||||
var value = command.TradeValuePolicy.Value;
|
||||
|
||||
switch (command.Kind)
|
||||
{
|
||||
case AetheriaRuntimeEveCommandKind.SetTradeValueQualityMinimum:
|
||||
policy.QualityPriceModifier.Minimum = ClampFinite(value, 0, double.MaxValue);
|
||||
persistPolicy = true;
|
||||
break;
|
||||
case AetheriaRuntimeEveCommandKind.SetTradeValueQualityMaximum:
|
||||
policy.QualityPriceModifier.Maximum = ClampFinite(value, 0, double.MaxValue);
|
||||
persistPolicy = true;
|
||||
break;
|
||||
case AetheriaRuntimeEveCommandKind.SetTradeValueQualityExponent:
|
||||
policy.QualityPriceModifier.Exponent = ClampFinite(value, 0.001, double.MaxValue);
|
||||
persistPolicy = true;
|
||||
break;
|
||||
case AetheriaRuntimeEveCommandKind.SetTradeValueTierQuality:
|
||||
var tierIndex = command.TradeValuePolicy.TierIndex;
|
||||
if (tierIndex >= 0 && tierIndex < policy.Tiers.Length)
|
||||
{
|
||||
policy.Tiers[tierIndex].Quality = ClampFinite(value, 0, 1);
|
||||
persistPolicy = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (persistPolicy)
|
||||
{
|
||||
policy.UpdatedAtUtc = command.IssuedAtUtc;
|
||||
await node.PutTradeValuePolicyAsync(policy).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Cycle(string current, params string[] values)
|
||||
{
|
||||
if (values.Length == 0)
|
||||
|
|
@ -295,6 +348,17 @@ public static class AetheriaEveCommandBridge
|
|||
return values[(index + 1) % values.Length];
|
||||
}
|
||||
|
||||
private static double ClampFinite(double value, double min, double max)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
return min;
|
||||
if (value < min)
|
||||
return min;
|
||||
if (value > max)
|
||||
return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void RecordRejection(
|
||||
AetheriaEveCommandAcceptanceReport report,
|
||||
AetheriaRuntimeEveCommandDocument command,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ public static class AetheriaOperationsSurfaceProjector
|
|||
eveCommandStatus?.LastPollAtUtc,
|
||||
normalizedVerseHost.LastUpdatedAtUtc,
|
||||
runtimeSession?.LastSeenAtUtc);
|
||||
var refreshCommand = GameCult.Mesh.CultMesh.OperationBindingRecord(
|
||||
GameCult.Mesh.CultMesh.OperationBinding(
|
||||
AetheriaRuntimeOperationsCommands.Refresh,
|
||||
label: "Refresh",
|
||||
routeHint: new GameCult.Mesh.CultMeshRouteHint(
|
||||
GameCult.Mesh.CultMeshLocalityKind.Automatic,
|
||||
"cultmesh")));
|
||||
return new EveSurfaceState
|
||||
{
|
||||
ProviderId = "aetheria",
|
||||
|
|
@ -85,9 +92,12 @@ public static class AetheriaOperationsSurfaceProjector
|
|||
[
|
||||
new EveCommandTemplate
|
||||
{
|
||||
Command = AetheriaRuntimeOperationsCommands.Refresh,
|
||||
Label = "Refresh",
|
||||
Transport = "cultmesh"
|
||||
Command = refreshCommand.OperationId,
|
||||
Label = refreshCommand.Label,
|
||||
Transport = refreshCommand.RouteDescription,
|
||||
SchemaId = refreshCommand.SchemaId,
|
||||
RouteKind = refreshCommand.RouteKind,
|
||||
RouteDescription = refreshCommand.RouteDescription
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -55,11 +55,18 @@ public static class AetheriaPlayerSettingsSurfaceProjector
|
|||
.ToArray()
|
||||
},
|
||||
Commands = surface.Commands
|
||||
.Select(command => new EveCommandTemplate
|
||||
.Select(command =>
|
||||
{
|
||||
Command = command.Command,
|
||||
Label = command.Label,
|
||||
Transport = command.Transport
|
||||
var record = GameCult.Mesh.CultMesh.OperationBindingRecord(command.Operation);
|
||||
return new EveCommandTemplate
|
||||
{
|
||||
Command = record.OperationId,
|
||||
Label = record.Label,
|
||||
Transport = record.RouteDescription,
|
||||
SchemaId = record.SchemaId,
|
||||
RouteKind = record.RouteKind,
|
||||
RouteDescription = record.RouteDescription
|
||||
};
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public static class AetheriaProviderAdvertisementProjector
|
|||
"aetheria.item_definition.v1",
|
||||
"aetheria.corporation.v2",
|
||||
"aetheria.name_file.v2",
|
||||
"aetheria.trade_value_policy.v1",
|
||||
"aetheria.player_settings.v1",
|
||||
"aetheria.loadout_template.v1",
|
||||
"aetheria.run_state.v1",
|
||||
|
|
|
|||
|
|
@ -16,11 +16,18 @@ public static class AetheriaRuntimeEveSurfaceStateProjector
|
|||
UpdatedAtUtc = document.UpdatedAtUtc,
|
||||
Surface = ToSurface(document.Surface),
|
||||
Commands = document.Commands
|
||||
.Select(command => new EveCommandTemplate
|
||||
.Select(command =>
|
||||
{
|
||||
Command = command.Command,
|
||||
Label = command.Label,
|
||||
Transport = command.Transport
|
||||
var record = GameCult.Mesh.CultMesh.OperationBindingRecord(command.Operation);
|
||||
return new EveCommandTemplate
|
||||
{
|
||||
Command = record.OperationId,
|
||||
Label = record.Label,
|
||||
Transport = record.RouteDescription,
|
||||
SchemaId = record.SchemaId,
|
||||
RouteKind = record.RouteKind,
|
||||
RouteDescription = record.RouteDescription
|
||||
};
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
|
|
@ -44,12 +51,30 @@ public static class AetheriaRuntimeEveSurfaceStateProjector
|
|||
|
||||
private static EveSurfaceComponent ToComponent(AetheriaRuntimeSurfaceComponent component)
|
||||
{
|
||||
var props = new Dictionary<string, string>(component.Props, StringComparer.Ordinal);
|
||||
AetheriaRuntimeSurfaceStateBindings.AddPointerProps(props, component.StateBindings);
|
||||
|
||||
return new EveSurfaceComponent
|
||||
{
|
||||
Id = component.Id,
|
||||
Kind = component.Kind,
|
||||
Props = new Dictionary<string, string>(component.Props, StringComparer.Ordinal),
|
||||
Children = component.Children.Select(ToComponent).ToArray()
|
||||
Props = props,
|
||||
Children = component.Children.Select(ToComponent).ToArray(),
|
||||
StateBindings = component.StateBindings
|
||||
.Select(binding =>
|
||||
{
|
||||
var record = GameCult.Mesh.CultMesh.StateBindingRecord(binding);
|
||||
return new EveSurfaceStateBinding
|
||||
{
|
||||
TargetProp = record.TargetProp,
|
||||
PointerId = record.PointerId,
|
||||
SourceId = record.SourceId,
|
||||
SchemaId = record.SchemaId,
|
||||
RouteKind = record.RouteKind,
|
||||
RouteDescription = record.RouteDescription
|
||||
};
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,53 @@ namespace Aetheria.State;
|
|||
|
||||
public static class AetheriaRuntimeStateMapper
|
||||
{
|
||||
public static AetheriaTradeValuePolicy ToTradeValuePolicy(
|
||||
AetheriaRuntimeTradeValueSettings settings,
|
||||
string updatedAtUtc)
|
||||
{
|
||||
return new AetheriaTradeValuePolicy
|
||||
{
|
||||
QualityPriceModifier = new AetheriaExponentialLerp
|
||||
{
|
||||
Exponent = settings.QualityPriceModifier.Exponent,
|
||||
Minimum = settings.QualityPriceModifier.Minimum,
|
||||
Maximum = settings.QualityPriceModifier.Maximum
|
||||
},
|
||||
Tiers = settings.Tiers
|
||||
.Select(tier => new AetheriaItemRarityTier
|
||||
{
|
||||
Name = tier.Name,
|
||||
Quality = tier.Quality,
|
||||
Red = tier.Red,
|
||||
Green = tier.Green,
|
||||
Blue = tier.Blue
|
||||
})
|
||||
.ToArray(),
|
||||
UpdatedAtUtc = updatedAtUtc
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeTradeValueSettings ToRuntimeTradeValueSettings(
|
||||
AetheriaTradeValuePolicy? policy)
|
||||
{
|
||||
if (policy == null)
|
||||
return AetheriaRuntimeTradeValueSettings.Default;
|
||||
|
||||
return new AetheriaRuntimeTradeValueSettings(
|
||||
new AetheriaRuntimeExponentialLerp(
|
||||
policy.QualityPriceModifier?.Exponent ?? 1,
|
||||
policy.QualityPriceModifier?.Minimum ?? 1,
|
||||
policy.QualityPriceModifier?.Maximum ?? 1),
|
||||
(policy.Tiers ?? Array.Empty<AetheriaItemRarityTier>())
|
||||
.Select(tier => new AetheriaRuntimeItemRarityTier(
|
||||
tier.Name,
|
||||
tier.Quality,
|
||||
tier.Red,
|
||||
tier.Green,
|
||||
tier.Blue))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
public static AetheriaLoadoutTemplate ToLoadoutTemplate(
|
||||
AetheriaRuntimeLoadoutTemplateCommit loadout,
|
||||
string updatedAtUtc)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
public static async Task<AetheriaStateNode> OpenAsync(
|
||||
string statePath,
|
||||
string runtimeId = "aetheria-local",
|
||||
bool startServer = false)
|
||||
bool startServer = false,
|
||||
bool enableDurableShardLogs = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(statePath))
|
||||
{
|
||||
|
|
@ -46,7 +47,7 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
new CultMeshNodeOptions
|
||||
{
|
||||
StartServer = startServer,
|
||||
EnableDurableShardLogs = true,
|
||||
EnableDurableShardLogs = enableDurableShardLogs,
|
||||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = cacheRegistry,
|
||||
|
|
@ -163,7 +164,20 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
return new AetheriaCatalogSnapshot(
|
||||
Cache.GetAll<AetheriaItemDefinition>(),
|
||||
Cache.GetAll<AetheriaCorporation>(),
|
||||
Cache.GetAll<AetheriaNameFile>());
|
||||
Cache.GetAll<AetheriaNameFile>(),
|
||||
Cache.GetAll<AetheriaTradeValuePolicy>().FirstOrDefault());
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<AetheriaTradeValuePolicy>> PutTradeValuePolicyAsync(
|
||||
AetheriaTradeValuePolicy policy)
|
||||
{
|
||||
return Database.PutAsync(new CultRecordKey(AetheriaTradeValuePolicy.RecordKey), policy);
|
||||
}
|
||||
|
||||
public Task<AetheriaTradeValuePolicy?> GetTradeValuePolicyAsync()
|
||||
{
|
||||
return Database.GetAsync<AetheriaTradeValuePolicy>(
|
||||
new CultRecordKey(AetheriaTradeValuePolicy.RecordKey));
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<EveSurfaceState>> PutCatalogSurfaceAsync(EveSurfaceState surface)
|
||||
|
|
@ -266,6 +280,49 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<AetheriaRuntimeCommittedCommandFactDocument>> PutCommittedCommandFactAsync(
|
||||
AetheriaRuntimeCommittedCommandFactDocument fact)
|
||||
{
|
||||
if (fact == null) throw new ArgumentNullException(nameof(fact));
|
||||
fact.Schema = AetheriaRuntimeDaemonSchemas.CommittedCommandFact;
|
||||
if (string.IsNullOrWhiteSpace(fact.FactId))
|
||||
fact.FactId = Guid.NewGuid().ToString("N");
|
||||
if (string.IsNullOrWhiteSpace(fact.CommittedAtUtc))
|
||||
fact.CommittedAtUtc = DateTime.UtcNow.ToString("O");
|
||||
|
||||
return Database.PutAsync(
|
||||
new CultRecordKey(AetheriaRuntimeCommittedCommandFactDocument.CreateRecordKey(fact.FactId)),
|
||||
fact);
|
||||
}
|
||||
|
||||
public IReadOnlyList<AetheriaRuntimeCommittedCommandFactDocument> ReadCommittedCommandFacts()
|
||||
{
|
||||
return Cache
|
||||
.GetAll<AetheriaRuntimeCommittedCommandFactDocument>()
|
||||
.OrderBy(fact => fact.CommittedAtUtc ?? "", StringComparer.Ordinal)
|
||||
.ThenBy(fact => fact.FactId ?? "", StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<AetheriaRuntimeVerseAuthorityPolicyDocument>> PutVerseAuthorityPolicyAsync(
|
||||
AetheriaRuntimeVerseAuthorityPolicyDocument policy)
|
||||
{
|
||||
return Database.PutAsync(
|
||||
new CultRecordKey(AetheriaRuntimeVerseAuthorityPolicyDocument.DocumentKey),
|
||||
policy);
|
||||
}
|
||||
|
||||
public Task<AetheriaRuntimeVerseAuthorityPolicyDocument?> GetVerseAuthorityPolicyAsync()
|
||||
{
|
||||
return Database.GetAsync<AetheriaRuntimeVerseAuthorityPolicyDocument>(
|
||||
new CultRecordKey(AetheriaRuntimeVerseAuthorityPolicyDocument.DocumentKey));
|
||||
}
|
||||
|
||||
public IReadOnlyList<AetheriaRuntimeAuthorityLeaseDocument> ReadAuthorityLeases()
|
||||
{
|
||||
return Cache.GetAll<AetheriaRuntimeAuthorityLeaseDocument>().ToArray();
|
||||
}
|
||||
|
||||
private static CultRecordKey DaemonCommandKey(string commandId)
|
||||
{
|
||||
return new CultRecordKey($"daemon:commands:{StableToken(commandId)}:gamecult.aetheria.daemon_command.v1");
|
||||
|
|
@ -324,6 +381,30 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
new CultRecordKey("daemon:aetheria.soa_view.latest.v1"));
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<AetheriaRuntimeStarbridgeScenarioDocument>> PutStarbridgeScenarioAsync(
|
||||
AetheriaRuntimeStarbridgeScenarioDocument scenario)
|
||||
{
|
||||
return Database.PutAsync(AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest, scenario);
|
||||
}
|
||||
|
||||
public Task<AetheriaRuntimeStarbridgeScenarioDocument?> GetStarbridgeScenarioAsync()
|
||||
{
|
||||
return Database.GetAsync<AetheriaRuntimeStarbridgeScenarioDocument>(
|
||||
AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest);
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<AetheriaRuntimeStarbridgeSessionDocument>> PutStarbridgeSessionAsync(
|
||||
AetheriaRuntimeStarbridgeSessionDocument session)
|
||||
{
|
||||
return Database.PutAsync(AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest, session);
|
||||
}
|
||||
|
||||
public Task<AetheriaRuntimeStarbridgeSessionDocument?> GetStarbridgeSessionAsync()
|
||||
{
|
||||
return Database.GetAsync<AetheriaRuntimeStarbridgeSessionDocument>(
|
||||
AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest);
|
||||
}
|
||||
|
||||
public Task<CultRecordHandle<EveSurfaceState>> PutDaemonGameSurfaceAsync(EveSurfaceState surface)
|
||||
{
|
||||
return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameSurfaceKey), surface);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using Aetheria.State.Documents;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using GameCult.Mesh;
|
||||
|
|
@ -15,13 +17,91 @@ public static class AetheriaVerseReplica
|
|||
string endpoint,
|
||||
string runtimeId = "aetheria-verse-replica")
|
||||
{
|
||||
using var node = await OpenReplicaNodeAsync(replicaStatePath, endpoint, runtimeId).ConfigureAwait(false);
|
||||
var shard = BuildReplicaShard(endpoint);
|
||||
var fetcher = new CultNetSchemaShardSnapshotFetcher();
|
||||
var snapshot = await fetcher.FetchAsync(shard).ConfigureAwait(false);
|
||||
var appliedSequence = await node.Database.ApplyShardSnapshotResponseAsync(shard, snapshot).ConfigureAwait(false);
|
||||
using var node = await OpenReplicaNodeAsync(
|
||||
replicaStatePath,
|
||||
endpoint,
|
||||
runtimeId,
|
||||
pullOnOpen: false).ConfigureAwait(false);
|
||||
return await SyncRawSnapshotChunksAsync(node, endpoint).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<int> SyncScopedSnapshotAsync(
|
||||
string replicaStatePath,
|
||||
string endpoint,
|
||||
string runtimeId = "aetheria-verse-replica",
|
||||
IReadOnlyList<string>? schemaIds = null,
|
||||
IReadOnlyList<string>? recordKeys = null,
|
||||
TimeSpan? connectTimeout = null,
|
||||
TimeSpan? responseTimeout = null)
|
||||
{
|
||||
using var node = await OpenReplicaNodeAsync(
|
||||
replicaStatePath,
|
||||
endpoint,
|
||||
runtimeId,
|
||||
pullOnOpen: false).ConfigureAwait(false);
|
||||
using var client = await ConnectedSnapshotClient.ConnectAsync(
|
||||
endpoint,
|
||||
connectTimeout ?? TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
||||
var snapshot = await client.RequestAsync(
|
||||
schemaIds,
|
||||
recordKeys,
|
||||
responseTimeout ?? TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
||||
var applied = await node.Database.Documents
|
||||
.ApplyRawSnapshotResponseAsync(node.Cache, snapshot)
|
||||
.ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
return appliedSequence;
|
||||
return applied.Count;
|
||||
}
|
||||
|
||||
public static async Task<CultNetSnapshotResponseRawMessage> FetchScopedSnapshotAsync(
|
||||
string endpoint,
|
||||
IReadOnlyList<string>? schemaIds = null,
|
||||
IReadOnlyList<string>? recordKeys = null,
|
||||
TimeSpan? connectTimeout = null,
|
||||
TimeSpan? responseTimeout = null)
|
||||
{
|
||||
using var client = await ConnectedSnapshotClient.ConnectAsync(
|
||||
endpoint,
|
||||
connectTimeout ?? TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
||||
return await client.RequestAsync(
|
||||
schemaIds,
|
||||
recordKeys,
|
||||
responseTimeout ?? TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<IReadOnlyList<T>> FetchScopedDocumentsAsync<T>(
|
||||
string endpoint,
|
||||
IReadOnlyList<string>? schemaIds = null,
|
||||
IReadOnlyList<string>? recordKeys = null,
|
||||
TimeSpan? connectTimeout = null,
|
||||
TimeSpan? responseTimeout = null)
|
||||
where T : class
|
||||
{
|
||||
var snapshot = await FetchScopedSnapshotAsync(
|
||||
endpoint,
|
||||
schemaIds,
|
||||
recordKeys,
|
||||
connectTimeout,
|
||||
responseTimeout).ConfigureAwait(false);
|
||||
var registry = AetheriaDocumentRegistry.CreateCultNetRegistry();
|
||||
var documents = new List<T>(snapshot.Documents.Length);
|
||||
foreach (var record in snapshot.Documents)
|
||||
{
|
||||
if (record == null)
|
||||
continue;
|
||||
|
||||
var binding = registry.GetBySchemaId(record.SchemaId);
|
||||
if (binding == null || binding.DocumentType != typeof(T))
|
||||
continue;
|
||||
|
||||
if (!string.Equals(record.PayloadEncoding, "messagepack", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException(
|
||||
$"CultNet raw document payloadEncoding must be \"messagepack\", not \"{record.PayloadEncoding}\".");
|
||||
|
||||
documents.Add((T)binding.PayloadDeserializer(record.Payload));
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
public static async Task RunReplicaAsync(
|
||||
|
|
@ -31,14 +111,15 @@ public static class AetheriaVerseReplica
|
|||
CancellationToken cancellationToken,
|
||||
string runtimeId = "aetheria-verse-replica")
|
||||
{
|
||||
using var node = await OpenReplicaNodeAsync(replicaStatePath, endpoint, runtimeId).ConfigureAwait(false);
|
||||
var shard = BuildReplicaShard(endpoint);
|
||||
using var node = await OpenReplicaNodeAsync(
|
||||
replicaStatePath,
|
||||
endpoint,
|
||||
runtimeId,
|
||||
pullOnOpen: false).ConfigureAwait(false);
|
||||
var snapshotFetcher = new CultNetSchemaShardSnapshotFetcher();
|
||||
var logFetcher = new CultNetSchemaShardLogFetcher();
|
||||
|
||||
var snapshot = await snapshotFetcher.FetchAsync(shard).ConfigureAwait(false);
|
||||
await node.Database.ApplyShardSnapshotResponseAsync(shard, snapshot).ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
await SyncRawSnapshotChunksAsync(node, endpoint).ConfigureAwait(false);
|
||||
|
||||
using var replicator = new CultNetShardReplicator(
|
||||
node.Database,
|
||||
|
|
@ -76,10 +157,237 @@ public static class AetheriaVerseReplica
|
|||
primaryEndpoints: new[] { endpoint.Trim() });
|
||||
}
|
||||
|
||||
private static async Task<long> SyncRawSnapshotChunksAsync(CultMeshNode node, string endpoint)
|
||||
{
|
||||
using var client = await ConnectedSnapshotClient.ConnectAsync(
|
||||
endpoint,
|
||||
TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
||||
var hot = await SyncHotPublicationsAsync(node, client).ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
return hot;
|
||||
}
|
||||
|
||||
private static async Task<long> SyncHotPublicationsAsync(CultMeshNode node, ConnectedSnapshotClient client)
|
||||
{
|
||||
var recordKeys = new[]
|
||||
{
|
||||
AetheriaRuntimeVerseRecordKeys.DaemonProviderAdvertisement.ToString(),
|
||||
AetheriaRuntimeVerseRecordKeys.DaemonHealth.ToString(),
|
||||
AetheriaRuntimeVerseRecordKeys.DaemonCommandBoundary.ToString(),
|
||||
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString(),
|
||||
AetheriaRuntimeVerseAuthorityPolicyDocument.DocumentKey,
|
||||
"daemon:aetheria.starbridge.session.latest.v1"
|
||||
};
|
||||
|
||||
var sequence = 0L;
|
||||
foreach (var recordKey in recordKeys)
|
||||
{
|
||||
var applied = await SyncScopedSnapshotAsync(
|
||||
node,
|
||||
client,
|
||||
schemaIds: null,
|
||||
recordKeys: new[] { recordKey },
|
||||
responseTimeout: TimeSpan.FromSeconds(15)).ConfigureAwait(false);
|
||||
sequence = Math.Max(sequence, SnapshotSequence(applied));
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
private static async Task<CultNetSnapshotResponseRawMessage> SyncScopedSnapshotAsync(
|
||||
CultMeshNode node,
|
||||
ConnectedSnapshotClient client,
|
||||
IReadOnlyList<string>? schemaIds,
|
||||
IReadOnlyList<string>? recordKeys,
|
||||
TimeSpan responseTimeout)
|
||||
{
|
||||
var snapshot = await client.RequestAsync(schemaIds, recordKeys, responseTimeout).ConfigureAwait(false);
|
||||
await node.Database.Documents.ApplyRawSnapshotResponseAsync(node.Cache, snapshot).ConfigureAwait(false);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static long SnapshotSequence(CultNetSnapshotResponseRawMessage snapshot)
|
||||
{
|
||||
return snapshot.ShardLogSequence ?? 0L;
|
||||
}
|
||||
|
||||
private sealed class ConnectedSnapshotClient : IDisposable
|
||||
{
|
||||
private const uint RtsConnectionId = 0x43554c54;
|
||||
private readonly ICultNetSchemaClient _client;
|
||||
private readonly string _endpoint;
|
||||
private readonly Dictionary<string, TaskCompletionSource<CultNetSnapshotResponseRawMessage>> _pending =
|
||||
new(StringComparer.Ordinal);
|
||||
private readonly object _pendingLock = new();
|
||||
private bool _disposed;
|
||||
|
||||
private ConnectedSnapshotClient(ICultNetSchemaClient client, string endpoint)
|
||||
{
|
||||
_client = client;
|
||||
_endpoint = endpoint;
|
||||
_client.OnCultNet<CultNetSnapshotResponseRawMessage>(OnSnapshotResponse);
|
||||
_client.OnCultNet<CultNetErrorMessage>(OnError);
|
||||
}
|
||||
|
||||
public static async Task<ConnectedSnapshotClient> ConnectAsync(string endpoint, TimeSpan connectTimeout)
|
||||
{
|
||||
var (host, port) = ParseEndpoint(endpoint);
|
||||
var client = string.Equals(new Uri(endpoint).Scheme, "rudp", StringComparison.OrdinalIgnoreCase)
|
||||
? CultNetSchemaClients.CreateRudp(
|
||||
runtimeId: "aetheria-verse-replica",
|
||||
connectionId: RtsConnectionId,
|
||||
maxFragmentBytes: 1200)
|
||||
: CultNetSchemaClients.CreateForEndpoint(endpoint);
|
||||
client.Connect(host, port);
|
||||
var connected = new ConnectedSnapshotClient(client, endpoint);
|
||||
await connected.WaitForConnectionAsync(connectTimeout).ConfigureAwait(false);
|
||||
return connected;
|
||||
}
|
||||
|
||||
public async Task<CultNetSnapshotResponseRawMessage> RequestAsync(
|
||||
IReadOnlyList<string>? schemaIds,
|
||||
IReadOnlyList<string>? recordKeys,
|
||||
TimeSpan responseTimeout)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ConnectedSnapshotClient));
|
||||
|
||||
var messageId = $"aetheria-replica:{Guid.NewGuid():N}";
|
||||
var completion = new TaskCompletionSource<CultNetSnapshotResponseRawMessage>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
lock (_pendingLock)
|
||||
{
|
||||
_pending.Add(messageId, completion);
|
||||
}
|
||||
|
||||
_client.SendCultNet(new CultNetSnapshotRequestMessage
|
||||
{
|
||||
MessageId = messageId,
|
||||
SchemaIds = CleanFilter(schemaIds),
|
||||
RecordKeys = CleanFilter(recordKeys),
|
||||
ShardId = ReplicaShardId,
|
||||
ShardEpoch = 1
|
||||
});
|
||||
|
||||
var timeoutTask = Task.Delay(responseTimeout);
|
||||
var completed = await Task.WhenAny(completion.Task, timeoutTask).ConfigureAwait(false);
|
||||
if (completed != completion.Task)
|
||||
{
|
||||
lock (_pendingLock)
|
||||
{
|
||||
_pending.Remove(messageId);
|
||||
}
|
||||
|
||||
throw new TimeoutException(
|
||||
$"Timed out waiting for shard snapshot response from {_endpoint} " +
|
||||
$"for schemas [{string.Join(", ", CleanFilter(schemaIds) ?? Array.Empty<string>())}] " +
|
||||
$"and records [{string.Join(", ", CleanFilter(recordKeys) ?? Array.Empty<string>())}].");
|
||||
}
|
||||
|
||||
return await completion.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_client.Dispose();
|
||||
lock (_pendingLock)
|
||||
{
|
||||
foreach (var pending in _pending.Values)
|
||||
{
|
||||
pending.TrySetCanceled();
|
||||
}
|
||||
|
||||
_pending.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitForConnectionAsync(TimeSpan connectTimeout)
|
||||
{
|
||||
var deadline = DateTimeOffset.UtcNow + connectTimeout;
|
||||
while (!_client.Connected)
|
||||
{
|
||||
if (DateTimeOffset.UtcNow >= deadline)
|
||||
{
|
||||
throw new TimeoutException($"Timed out connecting to shard primary endpoint {_endpoint}.");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(25)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(75)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnSnapshotResponse(CultNetSnapshotResponseRawMessage response)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(response.MessageId))
|
||||
return;
|
||||
|
||||
TaskCompletionSource<CultNetSnapshotResponseRawMessage>? completion;
|
||||
lock (_pendingLock)
|
||||
{
|
||||
if (!_pending.Remove(response.MessageId, out completion))
|
||||
return;
|
||||
}
|
||||
|
||||
completion.TrySetResult(response);
|
||||
}
|
||||
|
||||
private void OnError(CultNetErrorMessage error)
|
||||
{
|
||||
List<TaskCompletionSource<CultNetSnapshotResponseRawMessage>> pending;
|
||||
lock (_pendingLock)
|
||||
{
|
||||
pending = _pending.Values.ToList();
|
||||
_pending.Clear();
|
||||
}
|
||||
|
||||
var exception = new InvalidOperationException(error.Error);
|
||||
foreach (var completion in pending)
|
||||
{
|
||||
completion.TrySetException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static string[]? CleanFilter(IReadOnlyList<string>? values)
|
||||
{
|
||||
if (values is not { Count: > 0 })
|
||||
return null;
|
||||
|
||||
var filtered = values
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
return filtered.Length == 0 ? null : filtered;
|
||||
}
|
||||
|
||||
private static (string Host, int Port) ParseEndpoint(string endpoint)
|
||||
{
|
||||
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri))
|
||||
{
|
||||
throw new FormatException($"CultNet endpoint '{endpoint}' must be an absolute URI.");
|
||||
}
|
||||
|
||||
if (!string.Equals(uri.Scheme, "rudp", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(uri.Scheme, "cultnet", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new FormatException($"CultNet endpoint '{endpoint}' must use cultnet://host:port or rudp://host:port.");
|
||||
}
|
||||
|
||||
if (uri.Port <= 0 || uri.Port > 65535)
|
||||
{
|
||||
throw new FormatException($"CultNet endpoint '{endpoint}' must include a valid port.");
|
||||
}
|
||||
|
||||
return (uri.Host, uri.Port);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<CultMeshNode> OpenReplicaNodeAsync(
|
||||
string replicaStatePath,
|
||||
string endpoint,
|
||||
string runtimeId)
|
||||
string runtimeId,
|
||||
bool pullOnOpen = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(replicaStatePath))
|
||||
throw new ArgumentException("Replica state path must be non-empty.", nameof(replicaStatePath));
|
||||
|
|
@ -95,7 +403,7 @@ public static class AetheriaVerseReplica
|
|||
CacheOptions = new CultCacheOpenOptions
|
||||
{
|
||||
Registry = cacheRegistry,
|
||||
PullOnOpen = true,
|
||||
PullOnOpen = pullOnOpen,
|
||||
StoreFlushOnDispose = true,
|
||||
UseDirectoryStore = true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ public sealed class AetheriaPlayerGameplaySettings
|
|||
|
||||
[Key(1)]
|
||||
public int SignificantDigits { get; set; } = 3;
|
||||
|
||||
[Key(2)]
|
||||
public float DefaultShutdownPerformance { get; set; } = 0.25f;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
@ -223,9 +226,6 @@ public sealed class AetheriaRunState
|
|||
[CultReference(typeof(AetheriaZoneState), many: true)]
|
||||
public string[] ZoneKeys { get; set; } = [];
|
||||
|
||||
[Key(8)]
|
||||
public AetheriaActionBarBinding[] ActionBarBindings { get; set; } = [];
|
||||
|
||||
[Key(9)]
|
||||
public AetheriaFactionRelationshipState[] FactionRelationships { get; set; } = [];
|
||||
|
||||
|
|
@ -936,28 +936,6 @@ public sealed class AetheriaBehaviorStateSnapshot
|
|||
public bool TurretControllerPredictShots { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaActionBarBinding
|
||||
{
|
||||
[Key(0)]
|
||||
public string ControlPath { get; set; } = "";
|
||||
|
||||
[Key(1)]
|
||||
public string Kind { get; set; } = "";
|
||||
|
||||
[Key(2)]
|
||||
public string TargetKey { get; set; } = "";
|
||||
|
||||
[Key(3)]
|
||||
public int EquipmentIndex { get; set; } = -1;
|
||||
|
||||
[Key(4)]
|
||||
public int BehaviorIndex { get; set; } = -1;
|
||||
|
||||
[Key(5)]
|
||||
public int WeaponGroup { get; set; } = -1;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaFactionRelationshipState
|
||||
{
|
||||
|
|
|
|||
36
Aetheria.State/Documents/AetheriaTradeValuePolicy.cs
Normal file
36
Aetheria.State/Documents/AetheriaTradeValuePolicy.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
|
||||
namespace Aetheria.State.Documents;
|
||||
|
||||
[CultDocument("aetheria.trade_value_policy", "aetheria.trade_value_policy.v1")]
|
||||
[CultGlobal]
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaTradeValuePolicy
|
||||
{
|
||||
public const string RecordKey = "global:aetheria.trade_value_policy.v1";
|
||||
|
||||
[Key(0)] public string Schema { get; set; } = "aetheria.trade_value_policy.v1";
|
||||
[Key(1)] public string Name { get; set; } = "Aetheria trade value policy";
|
||||
[Key(2)] public AetheriaExponentialLerp QualityPriceModifier { get; set; } = new();
|
||||
[Key(3)] public AetheriaItemRarityTier[] Tiers { get; set; } = [];
|
||||
[Key(4)] public string UpdatedAtUtc { get; set; } = "";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaExponentialLerp
|
||||
{
|
||||
[Key(0)] public double Exponent { get; set; }
|
||||
[Key(1)] public double Minimum { get; set; }
|
||||
[Key(2)] public double Maximum { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaItemRarityTier
|
||||
{
|
||||
[Key(0)] public string Name { get; set; } = "";
|
||||
[Key(1)] public double Quality { get; set; }
|
||||
[Key(2)] public double Red { get; set; }
|
||||
[Key(3)] public double Green { get; set; }
|
||||
[Key(4)] public double Blue { get; set; }
|
||||
}
|
||||
339
Aetheria.State/docs/cultmesh-cross-runtime-primitives.md
Normal file
339
Aetheria.State/docs/cultmesh-cross-runtime-primitives.md
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
# CultMesh Cross-Runtime Primitives
|
||||
|
||||
Aetheria is the shakedown cruise for CultMesh. The Rust rebuild should not turn every useful idea into Aetheria-only glue. If a pattern helps any typed Verse daemon expose state, operations, queries, native views, authority, or UI surfaces across runtimes, it belongs in the shared CultMesh/CultLib layer. CultMesh should be expansive enough that application code feels like it is holding managed native state, while the runtime handles locality, synchronization, authority, codecs, and cache layout.
|
||||
|
||||
The bar is deliberately high: CultMesh should feel managed, cozy, and almost unfairly ergonomic to the developer, while the runtime silently chooses the fastest valid path underneath. In-process calls, shared CultCache slabs, native slices, IPC, remote transport, and browser/WASM views are locality choices, not different application APIs. The developer writes against typed Verse concepts; CultMesh supplies the plush handles, reactive wrappers, generated operations, query surfaces, native views, and diagnostics that make those concepts feel obvious.
|
||||
|
||||
The promotion rule is simple: if a helper would make Aetheria, Brokkr, Odin,
|
||||
Bifrost, Eve, Ymir, or VoidBot nicer in the same way, it is not an Aetheria
|
||||
helper. Hoist it into the shared cross-runtime CultMesh/CultLib layer first,
|
||||
then let Aetheria consume it as ordinary infrastructure. Aetheria may define
|
||||
domain nouns and policies; CultMesh owns the managed-feeling primitive shape
|
||||
that makes those nouns feel native from C#, Rust, TS, Unity, browser/WASM, and
|
||||
tooling hosts.
|
||||
|
||||
This is not a minimalist transport library. CultMesh should grow the shared
|
||||
abstractions that make daemon state feel intimate: typed state pointers,
|
||||
reactive wrappers, query handles, operation handles, native slice descriptors,
|
||||
authority claims, locality routing, schema-generated facades, UI bindings,
|
||||
diagnostics, and deterministic math/query primitives. "Primitive" does not
|
||||
only mean a small value type; it means any reusable cross-runtime developer
|
||||
affordance that can be hoisted into the shared library layer. The goal is a
|
||||
managed, cozy developer surface that reads like ordinary domain code and
|
||||
compiles down to the fastest safe path the runtime can prove.
|
||||
|
||||
## Design Bar
|
||||
|
||||
1. Typed state is the default. User code should hold handles, pointers, operations, and query objects, not record keys, schema ids, slot numbers, or raw payload maps.
|
||||
2. The same semantic surface works in Rust, TS, Unity/C#, browser/WASM, Eve/CultUI, and native tools.
|
||||
3. Locality is invisible until the developer asks for diagnostics. Co-located runtimes get shared memory or direct calls; remote runtimes get transport; browser runtimes get WASM-compatible views.
|
||||
4. Authority is a first-class Verse primitive. Policies, leases, runtime roles, claims, and future quorum hooks should be reusable across daemons.
|
||||
5. UI surfaces can contain typed pointers into daemon state. The UI runtime resolves, watches, invalidates, and invokes typed operations automatically.
|
||||
6. Native views expose zero-copy or near-zero-copy slices when safe, with the same state semantics as remote query/subscription fallbacks.
|
||||
7. Schema evolution is part of the developer experience. Bindings and migration manifests come from one source instead of hand-maintained slot maps.
|
||||
|
||||
## Ownership Boundary
|
||||
|
||||
| Belongs in CultMesh/CultLib | Belongs in Aetheria |
|
||||
| --- | --- |
|
||||
| Typed document handles, record routing, schema ids, migrations, codecs. | Aetheria world, zones, entities, inventory, stats, equipment, factions, Starbridge scenario concepts. |
|
||||
| Typed operation handles, receipts, idempotency, authority metadata, prediction hooks. | Pilot, RTS, refit, trade, interaction, construction, combat, and editor operation vocabulary. |
|
||||
| Typed query surfaces, query planning, locality routing, subscription lifetimes. | Object viewport, gravity viewport, selected object, inventory, stat, station, encounter, and tactical map queries. |
|
||||
| Reactive state pointers and UI binding semantics. | Aetheria Eve/CultUI surface content and domain-specific panel layout. |
|
||||
| Native slice descriptors, memory safety contracts, dirty ranges, runtime adapters. | Aetheria render columns, physics body columns, inventory/stat column definitions. |
|
||||
| Authority primitives: policy modes, leases, claims, runtime roles, diagnostics. | Which Aetheria operation kinds require which claims and which runtime cares most about each thing. |
|
||||
| CultMath primitives: `Vec2`, `Vec3`, `Rect`, `Circle`, `Sphere`, transforms, deterministic scalar helpers. | Aetheria coordinate conventions, body meanings, sensor ranges, gravity brush semantics. |
|
||||
| Provider discovery and interface binding conventions for Odin/Bifrost/Eve. | Aetheria provider advertisements, operation/query catalogs, and surface ids. |
|
||||
|
||||
## Primitive Set
|
||||
|
||||
First implementation footholds now exist in CultLib:
|
||||
|
||||
- `E:/Projects/CultLib/src/GameCult.Mesh/CultMeshPrimitives.cs` defines typed operation handles, query surfaces, state pointers, route hints, authority claims, and native slice descriptors.
|
||||
- `E:/Projects/CultLib/src/GameCult.Mesh/CultMeshPrimitives.cs` also exposes fluent operation/query context builders, with `CultMesh.OperationContextFor(...)` and `CultMesh.QueryContextFor(...)` facade entrypoints. Application code should read like a typed Verse call, not like manual transport setup.
|
||||
- `E:/Projects/CultLib/src/GameCult.Geometry/CultGeometryPrimitives.cs` defines shared `CultVec2`, `CultVec3`, `CultRect`, `CultCircle`, and `CultSphere` values for query and physics contracts.
|
||||
- `E:/Projects/CultLib/packages/cultmesh-ts/src/index.ts` now exposes TS `CultMeshVec2`, `CultMeshRect`, `CultMeshViewportRequest`, `CultMeshQuerySurface`, `CultMeshQueryContext`, `CultMeshOperationHandle`, `CultMeshOperationContext`, `CultMeshOperationPayload`, `CultMeshStatePointer`, `CultMeshStateBindingDescriptor`, `CultMeshOperationBindingDescriptor`, `CultMeshOperationInvocationDescriptor`, native slice descriptors, authority claims, route hints, fluent context builders, and facade helpers like `CultMesh.rectFromBounds(...)`, `CultMesh.viewportRequest(...)`, `CultMesh.query(...)`, `CultMesh.operation(...)`, `CultMesh.operationPayload(...)`, `CultMesh.statePointer(...)`, `CultMesh.stateBinding(...)`, `CultMesh.operationBinding(...)`, `CultMesh.operationInvocation(...)`, `CultMesh.nativeSliceView(...)`, `CultMesh.operationContextFor(...)`, and `CultMesh.queryContextFor(...)`.
|
||||
- `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimePlayerSettingsSurfaceBuilder.cs` and `E:/Projects/Eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs` now expose component `StateBindings` as `IReadOnlyList<CultMeshStateBindingDescriptor>`. Eve no longer owns a parallel live binding DTO, and Aetheria no longer keeps one in its live runtime surface model. Aetheria only keeps a MessagePack DTO at the persisted document boundary and converts to the shared CultMesh primitive before handing surfaces to renderers.
|
||||
- Aetheria and Eve live command templates now carry `CultMeshOperationBindingDescriptor`. Eve command requests carry `CultMeshOperationInvocationDescriptor` plus `CultMeshOperationPayload`, so renderer click/change events preserve operation id, schema, route hint, idempotency, and scalar field reads as shared CultMesh metadata. Legacy `command`, `label`, `transport`, and string payload fields remain compatibility projections and persisted DTO fields, but the live API no longer exposes raw command-string/dictionary constructors; controls point at typed CultMesh operations and renderers build requests from shared CultMesh primitives.
|
||||
- `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationsClient.cs` is now a typed operation facade that returns `CultMeshOperationReceipt` from semantic verbs such as `SetMoveVector`, `SetTarget`, `DockNearest`, and inventory transfers. The lower-level daemon command envelope remains an internal transport wrapper, but public client code should receive shared CultMesh receipts, not Aetheria-specific command envelopes.
|
||||
- `Packages/org.gamecult.aetheria.state/Runtime/AetheriaUi.cs` applies the same rule to Eve/CultUI commands. Unity UI and Eve presenter code should call `AetheriaClient.Ui` and receive `CultMeshOperationReceipt`; `AetheriaRuntimeEveCommandEnvelope` remains a bridge/document persistence detail for the Verse command boundary, not the shape application code leans on.
|
||||
- `AetheriaRuntimeEveCommands` is internal now. It may help smoke tests and internal bridge code manufacture persisted command documents, but it is not the public renderer/client API. Public callers use `AetheriaClient.Ui`, and the shared receipt is the public outcome type.
|
||||
- `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs` now exposes daemon render gravity/body query overloads around `CultMath.rect` in Aetheria XY space. `AetheriaRuntimeXzRect` remains only as a Unity legacy adapter.
|
||||
- `E:/Projects/CultLib/src/GameCult.Caching.MessagePack/DirectoryMessagePackBackingStore.cs` now recovers readable schema-stamped cold records when the hot directory manifest is missing the record's catalog entry. Aetheria hit this with persisted Verse authority policy state; the fix belongs in CultCache because durable Verse state should feel managed and resilient across runtime/schema refreshes, not like hand-maintained manifest bookkeeping.
|
||||
- `E:/Projects/CultLib/packages/cultcache-ts/src/single-file-messagepack-backing-store.ts` and `E:/Projects/CultLib/packages/cultcache-rs/src/lib.rs` now mirror the same schema-stamped recovery for single-file MessagePack snapshots. Their cache registries resolve recovered schema names and normalize envelopes back to registered public document types, so browser/Electron/Rust clients can share durable Verse state without depending on a stale hot catalog.
|
||||
- `E:/Projects/CultLib/packages/cultcache-py/src/cultcache_py/stores.py`, `E:/Projects/CultLib/packages/cultcache-py/src/cultnet_py/replication.py`, and `E:/Projects/CultLib/packages/cultcache-ts/src/cult-cache-inspector.ts` now carry the same primitive into Python stores, raw CultNet replication helpers, and tooling inspection. A stale schema id should be a recoverable runtime detail when the cold payload is schema-stamped, not a client-visible failure mode.
|
||||
|
||||
### Typed Document Handles
|
||||
|
||||
Current leak:
|
||||
|
||||
```ts
|
||||
await node.putRaw(recordKey, schemaId, msgpackBytes);
|
||||
const frame = decodeFrame(await node.get("daemon:aetheria.frame.latest.v1"));
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```ts
|
||||
const run = await verse.aetheria().currentRun().watch();
|
||||
const frame = await run.frame().latest();
|
||||
```
|
||||
|
||||
The handle owns record keys, schema ids, codec choice, migration readers, cache hydration, and subscription invalidation. The application code sees the typed value and its stable semantic location in the Verse.
|
||||
|
||||
### Typed Operation Handles
|
||||
|
||||
Current leak:
|
||||
|
||||
```ts
|
||||
await client.setMoveVector({
|
||||
actorEntityKey,
|
||||
directionX: input.x,
|
||||
directionY: input.y,
|
||||
scalar: 1,
|
||||
});
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```ts
|
||||
await verse
|
||||
.aetheria()
|
||||
.entity(actorEntityId)
|
||||
.pilot()
|
||||
.move({ x: input.x, y: input.y });
|
||||
```
|
||||
|
||||
CultMesh should generate operation handles from schema. The handle carries payload shape, authority claim metadata, routing, idempotency, receipts, diagnostics, and optional prediction hooks. The runtime decides whether this is a local direct call, a shared-slab write, a network operation, or a WASM host call. The RTS binding generator now emits `createAetheriaRuntimeRtsOperationHandles(...)` for `setMoveVector` and `setTarget`; the Electron client consumes those generated TS `CultMesh.operation(...)` handles while preserving the current daemon command document as an internal transport detail.
|
||||
|
||||
### Typed Query Surfaces
|
||||
|
||||
Current leak:
|
||||
|
||||
```ts
|
||||
const frame = await client.latestFrame();
|
||||
const objects = projectObjectsViewport(frame, rect, controlledEntityIds);
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```ts
|
||||
const objects = await verse
|
||||
.aetheria()
|
||||
.zone(zoneId)
|
||||
.objects()
|
||||
.visibleTo(controlledEntityIds)
|
||||
.within(rect)
|
||||
.watch();
|
||||
```
|
||||
|
||||
Queries are derived-state surfaces, not bespoke HTTP endpoints or local projection helpers. CultMesh should own query identity, parameters, caching, invalidation, locality routing, and remote execution. A co-located daemon can execute directly over slabs; a remote peer can execute through the Verse; a browser can consume the same query shape through generated TS/WASM bindings. The RTS binding generator now emits projection response types, `createAetheriaRuntimeRtsQueryHandles(...)` for every RTS read surface, renderer `aetheria-rts-contract.ts`, `AetheriaRtsIpcChannels`, the preload bridge, and `registerAetheriaRtsIpcHandlers(...)`. Those generated read handles are backed by `CultMesh.projectionRecipe(...).asQuerySurface()` with explicit daemon frame, health, authority, and Starbridge source descriptors. The RTS client passes its local publication route into the generated handles once, then uses automatic query contexts so the recipe owns locality defaults. Because `CultMeshQuerySurface` preserves `sources` and `routeHint`, the RTS client can expose `projectionDiagnostics()` directly from generated handle metadata instead of maintaining a parallel diagnostic table. Electron and browser clients consume generated contracts instead of duplicating query/response shapes, IPC channel names, handler registration, or viewport methods as the primary abstraction.
|
||||
|
||||
### Reactive State Pointers
|
||||
|
||||
Current leak:
|
||||
|
||||
```csharp
|
||||
var resolver = AetheriaRuntimeVerseClient.CreateEveSurfaceStateRefResolver(statePath, runtimeId);
|
||||
var selected = await resolver.ResolveAsync(surface.StateRef);
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```ts
|
||||
surface.panel("Selection", {
|
||||
entity: verse.aetheria().selection().entity().pointer(),
|
||||
actions: verse.aetheria().selection().entity().operations(),
|
||||
});
|
||||
```
|
||||
|
||||
Eve/CultUI props should be allowed to hold typed state pointers and operation handles. The UI runtime resolves them, subscribes to them, handles unavailable state, invalidates stale values, and invokes typed operations without custom per-daemon adapters.
|
||||
|
||||
### Native Slice Views
|
||||
|
||||
Current leak:
|
||||
|
||||
```csharp
|
||||
var descriptor = await client.GetSoaViewAsync();
|
||||
var map = MemoryMappedFile.OpenExisting(descriptor.MemoryMapName);
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```csharp
|
||||
using var view = await verse.Aetheria()
|
||||
.Zone(zoneId)
|
||||
.RenderView()
|
||||
.AsNativeArrays();
|
||||
```
|
||||
|
||||
CultMesh/CultCache should define native view descriptors, dirty ranges, schema fingerprints, lifetimes, and safety rules. Unity gets `NativeArray<T>`-friendly views. Rust gets slices. Browser/WASM gets typed array views when co-located or a compatible copied view when remote. The semantic surface stays the same.
|
||||
|
||||
### Authority Primitives
|
||||
|
||||
Current leak:
|
||||
|
||||
```csharp
|
||||
var decision = AetheriaRuntimeAuthorityRouter.Authorize(policy, command, runtimeId, leases);
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```rust
|
||||
let receipt = verse
|
||||
.aetheria()
|
||||
.entity(actor)
|
||||
.pilot()
|
||||
.move(vec2)
|
||||
.with_authority(runtime.claims().local_responsiveness())
|
||||
.await?;
|
||||
```
|
||||
|
||||
Near-term shared-library sugar already exists in C# and TS:
|
||||
|
||||
```csharp
|
||||
var context = CultMesh.OperationContextFor("unity-raven")
|
||||
.Claim("pilot-control", shardId: "zone:raven")
|
||||
.Route(CultMeshLocalityKind.SharedMemory, "co-located Verse")
|
||||
.Idempotency("move:raven:1")
|
||||
.Build();
|
||||
```
|
||||
|
||||
```ts
|
||||
const context = CultMesh.operationContextFor("browser-starfire")
|
||||
.claim("simulation-authority", { shardId: "zone:starfire" })
|
||||
.route("wasm", "browser-local projection")
|
||||
.idempotency("move:starfire:1")
|
||||
.build();
|
||||
```
|
||||
|
||||
Authority is not topology. CultMesh should provide reusable policy modes, claim kinds, leases, runtime roles, denial reasons, receipts, and diagnostics. Aetheria configures which operation families require which claims. Future server-authoritative, trusted distributed, lease, browser-host, and quorum modes should use the same primitive vocabulary.
|
||||
|
||||
### Geometry And Math Values
|
||||
|
||||
Current leak:
|
||||
|
||||
```ts
|
||||
viewport({ minX, minY, maxX, maxY });
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```ts
|
||||
const rect = CultMesh.rect(CultMesh.vec2(x0, y0), CultMesh.vec2(x1, y1));
|
||||
await zone.gravity().influences().intersecting(rect);
|
||||
```
|
||||
|
||||
CultMath/CultMesh should provide deterministic cross-runtime primitives for vectors, rects, circles, spheres, transforms, scalar tolerances, and spatial query inputs. In CultLib C# this starts as `CultVec2`, `CultVec3`, `CultRect`, `CultCircle`, and `CultSphere` under `GameCult.Geometry`; in TS this starts as `CultMeshVec2`, `CultMeshRect`, and `CultMeshViewportRequest` in `cultmesh-ts`. SoA does not require splitting a vector into unrelated conceptual values; the layout can still be cache-efficient while the API treats position, velocity, and acceleration as meaningful vector values.
|
||||
|
||||
### Locality-Aware Routing
|
||||
|
||||
Current leak:
|
||||
|
||||
```ts
|
||||
await fetch("http://127.0.0.1:39217/ymir/overlap-sphere", ...);
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```ts
|
||||
const hits = await verse
|
||||
.ymir()
|
||||
.world(worldId)
|
||||
.overlapSphere({ center, radius })
|
||||
.layers("gameplay")
|
||||
.all();
|
||||
```
|
||||
|
||||
The caller asks for a typed operation or query. CultMesh chooses in-process, shared slab, IPC, network, RUDP, browser/WASM bridge, or remote Verse execution. The choice is observable for diagnostics, but it is not encoded into application logic.
|
||||
|
||||
### Schema Evolution
|
||||
|
||||
Current leak:
|
||||
|
||||
```ts
|
||||
const entityId = payload[ENTITY_ID_SLOT];
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```ts
|
||||
const entity = frame.entities.byId(entityId);
|
||||
```
|
||||
|
||||
Bindings should be generated from one schema source into Rust, TS, Unity/C#, browser/WASM, Eve/CultUI metadata, and migration manifests. MessagePack key order, schema ids, deprecated fields, compatibility readers, and version negotiation are shared machinery.
|
||||
|
||||
### Surface Bindings
|
||||
|
||||
Current leak:
|
||||
|
||||
```json
|
||||
{ "kind": "button", "command": "dock", "payload": { "entityId": 42 } }
|
||||
```
|
||||
|
||||
Desired shape:
|
||||
|
||||
```ts
|
||||
button({
|
||||
icon: "anchor",
|
||||
enabled: selected.docking().canDock(),
|
||||
onPress: selected.docking().dock(),
|
||||
});
|
||||
```
|
||||
|
||||
CultUI should bind to typed state and operation handles. A surface can be rendered in Unity, browser, terminal, or an MCP host without rewriting command payloads or state-ref resolution. Bifrost exposes daemon Verse tools through these provider-owned interfaces; it is not a Brokkr-specific socket wrapper.
|
||||
|
||||
The TS side now has the primitive shape too:
|
||||
|
||||
```ts
|
||||
const selected = CultMesh.statePointer(
|
||||
"aetheria.selection.current",
|
||||
() => selection.resolve(),
|
||||
(emit) => selection.watch(emit),
|
||||
);
|
||||
```
|
||||
|
||||
That pointer is still only a primitive, not the final generated Verse facade.
|
||||
The next step is generator-owned state paths, codecs, unavailable-state
|
||||
diagnostics, and UI binding metadata.
|
||||
|
||||
The RTS generated bindings now create state pointer handles for the daemon
|
||||
frame, health, authority policy, and Starbridge session documents, and the RTS
|
||||
surface catalog advertises them with the same shared-memory route and schema
|
||||
source metadata as the query handles. This is the proof shape Eve/CultUI and
|
||||
Bifrost should consume: inspect typed pointer surfaces, resolve/watch them
|
||||
through the local Verse, and leave record keys and daemon-specific resolver
|
||||
rules below the shared primitive layer.
|
||||
|
||||
Those generated pointers are now Verse-bound in the RTS facade as well. The
|
||||
Electron client supplies publication-reader resolvers once, generated code binds
|
||||
the pointers with `CultMesh.bindStatePointer(queryVerse, pointer)`, and callers
|
||||
can resolve the underlying daemon documents through the facade without manually
|
||||
constructing query contexts or knowing where the local publication files live.
|
||||
|
||||
## Aetheria Proofs
|
||||
|
||||
Aetheria should prove these primitives by forcing each client to use the same shared shape:
|
||||
|
||||
1. Rust Aetheria loads the measured fixture world and publishes typed state through CultMesh.
|
||||
2. Rust Ymir answers step, overlap, cast, contact, broadphase, sparse-cluster, and viewport-intersection queries through typed query surfaces.
|
||||
3. The browser RTS client renders object and gravity viewports through query handles and submits operations through typed handles.
|
||||
4. Unity consumes render and physics views as native slices while keeping gameplay authority out of the scene hierarchy.
|
||||
5. Eve/CultUI panels bind to state pointers and operation handles with no daemon-specific resolver.
|
||||
6. Bifrost MCP tools discover provider interfaces through Odin and invoke typed Verse operations/queries without knowing whether the live daemon is Brokkr, Aetheria, or something else.
|
||||
7. Authority diagnostics explain which runtime may author which claim without changing operation schemas.
|
||||
|
||||
## Performance Contract
|
||||
|
||||
The cozy API must not be a slow API.
|
||||
|
||||
- Handles should be allocation-light after binding.
|
||||
- Query parameter objects should be value-shaped and cacheable.
|
||||
- Native views should expose contiguous slabs and dirty ranges where co-location allows it.
|
||||
- Remote fallbacks should keep the same semantic API but may return copied values or streamed deltas.
|
||||
- Hot paths must support generated code, stable layout, pooled codecs, and deterministic math.
|
||||
- The runtime should expose diagnostics for route choice, copy count, slab identity, schema version, authority decision, and subscription churn.
|
||||
|
||||
## Build Implication
|
||||
|
||||
When the Rust rebuild needs a new adapter, ask whether that adapter is actually a missing CultMesh primitive. Aetheria should contain the game. CultMesh/CultLib should contain the magic that makes the game feel native from every runtime.
|
||||
484
Aetheria.State/docs/cultmesh-ergonomics-staged-migration-plan.md
Normal file
484
Aetheria.State/docs/cultmesh-ergonomics-staged-migration-plan.md
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
# CultMesh Ergonomics Staged Migration Plan
|
||||
|
||||
This is the staged plan for getting CultMesh ergonomics from "usable transport and replicated records" to the ideal developer experience: typed state that feels local, reactive UI surfaces that resolve daemon pointers automatically, native slab views for fast clients, and authority semantics that are explicit without making every client hand-roll routing code.
|
||||
|
||||
The Rust/WASM rebuild remains valuable, but it is not the MVP path. The near-term goal is to build the perfect machine in C# first, freeze the API shape we actually want, then let any later Rust body implement that shape rather than inherit the current leaks.
|
||||
|
||||
## North Star
|
||||
|
||||
C# clients should read like domain code:
|
||||
|
||||
```csharp
|
||||
var aetheria = verse.Aetheria();
|
||||
|
||||
await aetheria.Entity(actorEntityId)
|
||||
.Pilot
|
||||
.MoveAsync(CultMesh.Vec2(input.X, input.Y));
|
||||
|
||||
var objects = await aetheria.Zone(zoneId)
|
||||
.Objects
|
||||
.VisibleTo(controlledEntityIds)
|
||||
.Within(rect)
|
||||
.LatestAsync();
|
||||
|
||||
using var renderView = await aetheria.Zone(zoneId)
|
||||
.RenderView
|
||||
.AsNativeArraysAsync();
|
||||
```
|
||||
|
||||
TypeScript clients should have the same semantic shape:
|
||||
|
||||
```ts
|
||||
const aetheria = verse.aetheria();
|
||||
|
||||
await aetheria.entity(actorEntityId)
|
||||
.pilot.move(CultMesh.vec2(input.x, input.y));
|
||||
|
||||
const objects = await aetheria.zone(zoneId)
|
||||
.objects.visibleTo(controlledEntityIds)
|
||||
.within(rect)
|
||||
.latest();
|
||||
|
||||
const gravity = await aetheria.zone(zoneId)
|
||||
.gravity.influences()
|
||||
.intersecting(rect)
|
||||
.latest();
|
||||
```
|
||||
|
||||
Eve/CultUI surfaces should carry typed state pointers and operation handles, not stringly command documents:
|
||||
|
||||
```ts
|
||||
surface.panel("Selection", {
|
||||
entity: verse.aetheria().selection().entity().pointer(),
|
||||
stats: verse.aetheria().selection().entity().stats().pointer(),
|
||||
actions: verse.aetheria().selection().entity().operations(),
|
||||
});
|
||||
```
|
||||
|
||||
Ymir should be reachable through the same typed query surface pattern:
|
||||
|
||||
```ts
|
||||
const hits = await verse.ymir()
|
||||
.world(worldId)
|
||||
.overlapSphere({ center, radius })
|
||||
.layers("gameplay")
|
||||
.all();
|
||||
```
|
||||
|
||||
Authority should be claimed through typed operation context sugar, with CultMesh owning the generic primitive and Aetheria owning the policy:
|
||||
|
||||
```csharp
|
||||
await aetheria.Entity(actorEntityId)
|
||||
.Pilot
|
||||
.WithAuthority("pilot-control", shardId: $"entity:{actorEntityId}")
|
||||
.MoveAsync(CultMesh.Vec2(input.X, input.Y));
|
||||
```
|
||||
|
||||
## Current Baseline
|
||||
|
||||
The project already has the first layer of primitive sugar:
|
||||
|
||||
- `CultMeshOperationHandle`, `CultMeshOperationBindingDescriptor`, `CultMeshOperationBindingRecord`, `CultMeshOperationInvocationDescriptor`, `CultMeshOperationInvocationRecord`, and `CultMeshOperationPayload` exist in CultLib.
|
||||
- `CultMeshStatePointer`, state binding descriptors/records, route hints, authority claims, query surfaces, viewport requests, native slice descriptors, and context builders exist in C# and TypeScript.
|
||||
- CultMath/CultGeometry now has vector and rect primitives, including XY viewport vocabulary.
|
||||
- RTS bindings are generated from the Aetheria schema and expose a typed-ish Verse facade.
|
||||
- Aetheria Eve surfaces persist CultMesh binding/invocation records and keep legacy fields as compatibility mirrors.
|
||||
- `AetheriaClient`, `AetheriaControl`, and `AetheriaUi` expose ergonomic C# entry points, but the domain facade is still incomplete.
|
||||
|
||||
The remaining problem is that the sugar is not yet the only obvious path. There are still public or semi-public seams where clients can fall back into raw envelopes, record keys, ad hoc frame projection, manual state resolution, and locality-specific wiring.
|
||||
|
||||
## Migration Rules
|
||||
|
||||
1. Repeated Aetheria-specific glue becomes a generated Aetheria facade.
|
||||
2. Repeated cross-runtime glue becomes CultMesh/CultLib.
|
||||
3. Public client APIs must expose typed domain operations, typed query surfaces, typed pointers, or native views.
|
||||
4. Raw payload dictionaries, command names, schema ids, record keys, slot ids, and transport-local route details are allowed inside generated/runtime internals only.
|
||||
5. Compatibility mirrors are temporary scaffolding. They must have a named removal stage.
|
||||
6. Unity is a renderer/input client. It should not own gameplay state, zone hierarchy, simulation cadence, physics, or entity authority by accident.
|
||||
7. The browser/RTS client and Unity client must use the same semantic Verse facade, even when their locality/runtime routes differ.
|
||||
|
||||
## Stage 0: Freeze The Vocabulary
|
||||
|
||||
Status: mostly done, keep tightening.
|
||||
|
||||
Goal: make the desired surface explicit enough that every later stage can delete ad hoc code instead of inventing new abstractions in place.
|
||||
|
||||
Targets:
|
||||
|
||||
- Keep `Aetheria.State/docs/cultmesh-cross-runtime-primitives.md` as the primitive ownership boundary.
|
||||
- Keep `Aetheria.State/docs/daemon-code-map-for-rust-port.md` as the current daemon/control-flow map.
|
||||
- Treat this document as the staged migration checklist.
|
||||
- Define canonical names for:
|
||||
- document handles
|
||||
- operation handles
|
||||
- operation invocation records
|
||||
- query surfaces
|
||||
- state pointers
|
||||
- native slice views
|
||||
- authority claims
|
||||
- route hints
|
||||
- geometry values
|
||||
|
||||
Gates:
|
||||
|
||||
- No new public API may introduce `Apply(command, payload)` style call sites.
|
||||
- No new client API may require a caller to know transport record keys or raw schema slots.
|
||||
- All new examples in docs must use the North Star shape.
|
||||
|
||||
## Stage 1: Harden Shared CultMesh Primitives
|
||||
|
||||
Status: started.
|
||||
|
||||
Goal: make CultMesh primitives boring, portable, serializable, and pleasant enough that Aetheria does not need local substitutes.
|
||||
|
||||
Targets:
|
||||
|
||||
- C# primitives in `E:/Projects/CultLib/src/GameCult.Mesh/CultMeshPrimitives.cs`.
|
||||
- Geometry primitives in `E:/Projects/CultLib/src/GameCult.Geometry/CultGeometryPrimitives.cs`.
|
||||
- TS primitives in `E:/Projects/Aetheria/packages/cultmesh-ts/src/index.ts`.
|
||||
- MessagePack/JSON-compatible record forms for operation/state/query/native/authority values.
|
||||
- Typed payload readers and builders so domain facades do not pass `Dictionary<string, object?>` in public.
|
||||
- Stable route and authority record forms that can cross process/runtime boundaries.
|
||||
|
||||
Work:
|
||||
|
||||
- Add typed scalar/vector/list readers to operation payloads.
|
||||
- Add generated or handwritten conversion helpers between descriptor objects and record objects.
|
||||
- Add invariant tests for C#/TS round trips.
|
||||
- Keep primitive names runtime-neutral. Aetheria, Brokkr, Eve, Odin, VoidBot, and Ymir should all be able to use the same vocabulary.
|
||||
|
||||
Gates:
|
||||
|
||||
```powershell
|
||||
dotnet test E:\Projects\CultLib\tests\GameCult.Mesh.Tests\GameCult.Mesh.Tests.csproj --no-restore -v:minimal
|
||||
npm test --workspace packages/cultmesh-ts
|
||||
```
|
||||
|
||||
## Stage 2: Generate Domain Facades
|
||||
|
||||
Status: started in RTS bindings; incomplete across C#/Unity/Eve.
|
||||
|
||||
Goal: make the typed facade the public shape in every runtime.
|
||||
|
||||
Targets:
|
||||
|
||||
- C# facade: `verse.Aetheria().Entity(id).Pilot.MoveAsync(...)`.
|
||||
- TS facade: `verse.aetheria().entity(id).pilot.move(...)`.
|
||||
- Eve/CultUI bindings: surface actions are operation handles, not command strings.
|
||||
- Unity facade: Unity gameplay/input shell calls typed operations and typed queries only.
|
||||
|
||||
Work:
|
||||
|
||||
- Promote the current RTS binding generator into a general Aetheria CultMesh facade generator.
|
||||
- Generate C# and TS operation handles from the same schema.
|
||||
- Generate query builders for objects, gravity, inventory, selection, stats, action availability, and map/RTS visibility.
|
||||
- Generate state pointer builders for selected object, selected inventory, selected stats, UI panels, and live frame metadata.
|
||||
- Add a verifier that fails if public client code exposes command-name/payload pairs.
|
||||
|
||||
Desired C# shape:
|
||||
|
||||
```csharp
|
||||
await client.Aetheria()
|
||||
.Entity(actorEntityId)
|
||||
.Equipment(slot)
|
||||
.ActivateAsync(target);
|
||||
```
|
||||
|
||||
Desired TS shape:
|
||||
|
||||
```ts
|
||||
await client.aetheria()
|
||||
.entity(actorEntityId)
|
||||
.equipment(slot)
|
||||
.activate(target);
|
||||
```
|
||||
|
||||
Gates:
|
||||
|
||||
```powershell
|
||||
dotnet run --project E:\Projects\Aetheria\Aetheria.State.Verify\Aetheria.State.Verify.csproj --no-restore
|
||||
npm run generate:rts-bindings
|
||||
npm run check:rts-bindings
|
||||
```
|
||||
|
||||
## Stage 3: Make Query Surfaces The Client Contract
|
||||
|
||||
Status: partially started; local projection still leaks.
|
||||
|
||||
Goal: clients ask the daemon for typed projections. They do not reconstruct world truth from frames unless they are inside a generated/internal query executor.
|
||||
|
||||
Targets:
|
||||
|
||||
- Objects viewport.
|
||||
- Gravity viewport.
|
||||
- Visible set for RTS: union of entities visible to controlled units.
|
||||
- Selected object.
|
||||
- Current stats.
|
||||
- Inventory.
|
||||
- Equipment/action availability.
|
||||
- Station/service status.
|
||||
- Ymir overlap/cast queries.
|
||||
|
||||
Work:
|
||||
|
||||
- Move remaining browser-local projection paths behind generated query executors or daemon query endpoints.
|
||||
- Make viewport queries use `CultRect`/XY everywhere outside Unity adapter boundaries.
|
||||
- Give each query a stable typed request and typed response.
|
||||
- Add watch/latest semantics consistently:
|
||||
- `latest()` for one-shot reads.
|
||||
- `watch()` for reactive subscriptions.
|
||||
- `asNativeArrays()` where co-located slab views are available.
|
||||
|
||||
Gates:
|
||||
|
||||
- RTS client rendering uses query surfaces, not frame spelunking.
|
||||
- Unity rendering asks for render/query views, not a mirrored zone hierarchy.
|
||||
- `ZoneRenderer.LoadZone` and ActionGameManager-owned zone state become vestigial or disappear.
|
||||
- Public client code has no direct dependency on current daemon frame layout except through generated/internal projection code.
|
||||
|
||||
## Stage 4: Resolve Eve State Pointers Automatically
|
||||
|
||||
Status: primitive records exist; runtime resolution is not yet the whole story.
|
||||
|
||||
Goal: an Eve surface can contain a typed pointer into daemon state and the UI runtime resolves it, watches it, and binds it without custom per-panel plumbing.
|
||||
|
||||
Targets:
|
||||
|
||||
- Eve surface documents carry `CultMeshStateBindingRecord`.
|
||||
- Operation buttons carry `CultMeshOperationBindingRecord`.
|
||||
- User actions emit `CultMeshOperationInvocationRecord`.
|
||||
- CultUI runtime resolves pointers via the current Verse.
|
||||
- Unity UIToolkit lowerer consumes the same surface semantics as browser/Electron.
|
||||
|
||||
Work:
|
||||
|
||||
- Add a pointer resolver interface to CultMesh/CultUI rather than Aetheria-specific code.
|
||||
- Add watch lifetime management to UI runtime.
|
||||
- Add loading/error/stale states as generic pointer states.
|
||||
- Remove manual state-ref resolver glue from Aetheria UI code once the generic runtime path exists.
|
||||
|
||||
Gates:
|
||||
|
||||
- Selection panel can bind selected entity/stats/inventory from pointers only.
|
||||
- No Aetheria panel needs to manually decode a state reference.
|
||||
- Surface command persistence contains CultMesh invocation records as the authoritative form.
|
||||
|
||||
## Stage 5: Native View Sugar For Unity And Co-Located Clients
|
||||
|
||||
Status: designed; implementation still thin.
|
||||
|
||||
Goal: Unity becomes a thin, brutally fast renderer/input client over Aetheria/Ymir state.
|
||||
|
||||
Targets:
|
||||
|
||||
- `AsNativeArraysAsync()` over CultCache slabs.
|
||||
- Unity-safe lifetime wrappers around shared memory/native slice descriptors.
|
||||
- Burst-readable views for render columns and physics state.
|
||||
- Dirty ranges/version stamps so clients can skip unchanged slabs.
|
||||
- Render-oriented projections generated by Aetheria, not maintained as a Unity hierarchy.
|
||||
|
||||
Work:
|
||||
|
||||
- Define native slice descriptors for Aetheria render views and Ymir physics views.
|
||||
- Wrap descriptors in Unity `NativeArray<T>`/`NativeSlice<T>` adapters with explicit lifetime ownership.
|
||||
- Add schema/version validation before exposing a slab as a native view.
|
||||
- Move camera/frustum queries to daemon query surfaces.
|
||||
- Keep Unity-specific transform/material/sprite concerns in Unity, but pull entity membership, object location, gravity influence, physics state, inventory, and stats from Aetheria/Ymir.
|
||||
|
||||
Desired Unity shape:
|
||||
|
||||
```csharp
|
||||
using var view = await verse.Aetheria()
|
||||
.Zone(zoneId)
|
||||
.RenderView
|
||||
.ForCamera(cameraQuery)
|
||||
.AsNativeArraysAsync();
|
||||
|
||||
RenderJobs.Schedule(view.Objects, view.Sprites, view.Gravity);
|
||||
```
|
||||
|
||||
Gates:
|
||||
|
||||
```powershell
|
||||
dotnet build E:\Projects\Aetheria\GameCult.Aetheria.State.Unity.csproj --no-restore -v:minimal
|
||||
dotnet build E:\Projects\Aetheria\Aetheria.State.Unity.Smoke\Aetheria.State.Unity.Smoke.csproj --no-restore -v:minimal
|
||||
```
|
||||
|
||||
## Stage 6: Authority Sugar And Locality Policies
|
||||
|
||||
Status: primitive claims exist; gameplay policy needs first-class wiring.
|
||||
|
||||
Goal: authority is explicit and configurable without leaking into every gameplay call.
|
||||
|
||||
Targets:
|
||||
|
||||
- Server-authoritative mode.
|
||||
- Trusted co-op distributed simulation mode.
|
||||
- Future quorum/re-sim mode, without blocking MVP.
|
||||
- Per-shard/per-entity authority claims.
|
||||
- Route hints chosen by Verse/runtime policy.
|
||||
|
||||
Work:
|
||||
|
||||
- Add Aetheria authority policy config as data, not hardcoded branch logic.
|
||||
- Add facade sugar:
|
||||
- `WithAuthority(...)`
|
||||
- `PreferLocality(...)`
|
||||
- `AsNode(...)`
|
||||
- `WithIdempotency(...)`
|
||||
- Attach operation context automatically where a facade knows the actor/entity/shard.
|
||||
- Add diagnostics that explain why an operation routed to local/shared-memory/network/host.
|
||||
- Keep CultMesh generic: it owns claims, leases, routes, receipts, and diagnostics; Aetheria owns which operations require which claims.
|
||||
|
||||
Gates:
|
||||
|
||||
- Unity player can own simulation authority for its own pilot state in trusted co-op config.
|
||||
- RTS player can own hostile pawns or commanded pawns in trusted co-op config.
|
||||
- Server-authoritative config can force daemon/host-only simulation authority.
|
||||
- No public gameplay API requires manually constructing a command port.
|
||||
|
||||
## Stage 7: Ymir Query Facade And Physics Ownership
|
||||
|
||||
Status: Ymir owns more physics directionally, but query sugar and benchmark discipline need to catch up.
|
||||
|
||||
Goal: physics is a typed service with the same CultMesh ergonomics as Aetheria state.
|
||||
|
||||
Targets:
|
||||
|
||||
- `OverlapSphere`.
|
||||
- Brush/viewport intersection for gravity influence queries.
|
||||
- Collision/cast queries needed by abilities and movement.
|
||||
- Gravity terrain influence queries.
|
||||
- Physics benchmark suite with deterministic sparse clustered scenes.
|
||||
|
||||
Work:
|
||||
|
||||
- Add Ymir typed query handles using CultMesh query surface primitives.
|
||||
- Add C# and TS facade generation for Ymir.
|
||||
- Add Aetheria-to-Ymir state references rather than duplicating physics ownership in Unity.
|
||||
- Add benchmarks that compare vector SoA, scalar SoA, and spatial acceleration structures.
|
||||
- Keep Unity physics out of gameplay state. Unity may render/debug physics but not authoritatively simulate it.
|
||||
|
||||
Gates:
|
||||
|
||||
- Gameplay queries use Ymir facade operations.
|
||||
- Gravity viewport queries can ask for every body whose influence brush intersects an XY viewport.
|
||||
- Benchmarks are deterministic and run in CI or a documented local perf lane.
|
||||
|
||||
## Stage 8: One Schema Pipeline
|
||||
|
||||
Status: pieces exist; still too hand-maintained.
|
||||
|
||||
Goal: one schema source generates cross-runtime facades, record serializers, pointer/query/operation metadata, and docs.
|
||||
|
||||
Targets:
|
||||
|
||||
- C# facade generation.
|
||||
- TS facade generation.
|
||||
- Eve/CultUI binding metadata.
|
||||
- Unity native view adapters.
|
||||
- Query request/response records.
|
||||
- Operation payload records.
|
||||
- Schema evolution manifests.
|
||||
- Legacy readers only where explicitly needed.
|
||||
|
||||
Work:
|
||||
|
||||
- Replace one-off RTS binding generation with a CultMesh schema generator.
|
||||
- Generate operation payload types instead of exposing generic payload maps.
|
||||
- Generate query result types and pointer target types.
|
||||
- Generate docs/snippets from schema metadata so examples cannot rot.
|
||||
- Add schema version compatibility checks to native view and network decode paths.
|
||||
|
||||
Gates:
|
||||
|
||||
- Adding a new Aetheria operation once creates C#, TS, Eve operation binding, verifier metadata, and docs.
|
||||
- Adding a new query once creates C#, TS, Eve pointer/query metadata, and query executor stubs.
|
||||
- No runtime has a hand-authored duplicate of another runtime's binding vocabulary.
|
||||
|
||||
## Stage 9: Delete Compatibility Shells
|
||||
|
||||
Status: not started.
|
||||
|
||||
Goal: remove scaffolding once the generated typed path is the only supported path.
|
||||
|
||||
Remove:
|
||||
|
||||
- Public command-name/payload APIs.
|
||||
- Cached command port concepts.
|
||||
- Public raw frame spelunking for client projections.
|
||||
- Public record-key/schema-slot access in client code.
|
||||
- Manual state-ref resolution in UI panels.
|
||||
- Legacy XZ vocabulary outside Unity adapter names.
|
||||
- Unity-owned zone hierarchy/gameplay simulation state.
|
||||
- Browser-local bespoke gameplay behavior.
|
||||
|
||||
Keep only as internals where required:
|
||||
|
||||
- Wire-level records.
|
||||
- Legacy readers for persisted documents that predate the migration.
|
||||
- Diagnostic tools that intentionally expose raw records.
|
||||
- Tests that assert legacy decode behavior until the migration window closes.
|
||||
|
||||
Gates:
|
||||
|
||||
- Verifier blocks new public stringly operation surfaces.
|
||||
- `rg` sweeps show no client-facing `Apply(command`, `commandName`, `payload`, `recordKey`, or raw slot access outside generated/runtime internals.
|
||||
- Unity, RTS/Electron, and Eve all consume the same semantic facade.
|
||||
|
||||
## Stage 10: MVP Acceptance
|
||||
|
||||
The migration is MVP-complete when:
|
||||
|
||||
- Aetheria daemon owns gameplay simulation state.
|
||||
- Ymir owns physics state and gameplay physics queries.
|
||||
- Unity is rendering/input focused.
|
||||
- RTS/Electron is a thin CultMesh client, not a parallel gameplay implementation.
|
||||
- Eve/CultUI surfaces use typed state pointers and typed operation handles.
|
||||
- Aetheria exposes current stats, inventory, equipment, selection, objects viewport, gravity viewport, and authority-aware operations as typed surfaces.
|
||||
- Unity and browser clients can point at the same Verse and observe/interact with the same simulation according to authority policy.
|
||||
- Co-located clients can use native slab views where available.
|
||||
- Remote clients use the same facade shape over network routes.
|
||||
- Locality is visible in diagnostics, not application logic.
|
||||
|
||||
## Non-Goals For This MVP
|
||||
|
||||
- Rebuilding the daemon in Rust.
|
||||
- Making the browser host the full simulation in WASM.
|
||||
- Quorum/re-sim consensus implementation.
|
||||
- Perfect zero-copy support across every runtime.
|
||||
- Exact parity with every historical Unity behavior.
|
||||
|
||||
These are future expansion paths. The migration must not close the door on them, but it should not block the MVP on them.
|
||||
|
||||
## Verification Lane
|
||||
|
||||
Use this lane when changing the sugar surface:
|
||||
|
||||
```powershell
|
||||
dotnet test E:\Projects\CultLib\tests\GameCult.Mesh.Tests\GameCult.Mesh.Tests.csproj --no-restore -v:minimal
|
||||
dotnet run --project E:\Projects\Aetheria\Aetheria.State.Verify\Aetheria.State.Verify.csproj --no-restore
|
||||
dotnet build E:\Projects\Aetheria\GameCult.Aetheria.State.Unity.csproj --no-restore -v:minimal
|
||||
dotnet build E:\Projects\Aetheria\Aetheria.State.Unity.Smoke\Aetheria.State.Unity.Smoke.csproj --no-restore -v:minimal
|
||||
npm run generate:rts-bindings
|
||||
npm run check:rts-bindings
|
||||
npm run build
|
||||
```
|
||||
|
||||
Add Ymir benchmark and authority smoke commands to this lane as those stages land.
|
||||
|
||||
## Sugar Targets Checklist
|
||||
|
||||
- [ ] Typed Aetheria C# domain facade is complete.
|
||||
- [ ] Typed Aetheria TS domain facade is complete.
|
||||
- [ ] Typed Ymir C# and TS query facades exist.
|
||||
- [ ] Eve/CultUI resolves CultMesh state pointers automatically.
|
||||
- [ ] Eve/CultUI invokes operation records without Aetheria-specific command glue.
|
||||
- [ ] Query surfaces cover objects viewport, gravity viewport, selection, stats, inventory, equipment, and services.
|
||||
- [ ] Unity render path consumes native views or query results, not gameplay-owned mirrored state.
|
||||
- [ ] RTS render path consumes query surfaces, not bespoke frame projection as public client behavior.
|
||||
- [ ] Authority policy is data-driven per Verse.
|
||||
- [ ] Operation context sugar attaches route, claim, and idempotency without caller boilerplate.
|
||||
- [ ] Schema generator emits C#, TS, Eve metadata, query records, operation payload records, and docs.
|
||||
- [ ] Public APIs do not expose stringly command names or raw payload dictionaries.
|
||||
- [ ] Compatibility mirrors are internal, named, and scheduled for deletion.
|
||||
551
Aetheria.State/docs/daemon-code-map-for-rust-port.md
Normal file
551
Aetheria.State/docs/daemon-code-map-for-rust-port.md
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
# Aetheria Daemon Code Map for Rust Rebuild
|
||||
|
||||
This is a map of the current C# daemon as a reference API to document, measure, and deprecate before the rebuild. This is not an Agile migration plan and not a request to preserve the current architecture. The C# code is the shakedown cruise: it taught us where Unity, TS, browser, Eve/CultUI, Rust, and native clients need clean CultMesh ergonomics, where the daemon leaked local-process assumptions, and where the API wants to become magical instead of merely serviceable.
|
||||
|
||||
The rebuild target is a clean cross-runtime Aetheria surface: a Rust-native daemon and Ymir body, with useful abstractions hoisted into CultMesh/CultLib so typed state, operations, queries, native slices, authority, and UI state pointers are shared library primitives. Clients should feel like they are holding reactive, native Aetheria state, not hand-pulling snapshots, decoding array slots, reading local publication files, or building bespoke command packets.
|
||||
|
||||
This also means Ymir needs a Rust body. Aetheria cannot become a Rust-native simulation daemon while physics remains a C# or Unity-shaped side service. The C# Ymir contracts and Unity bridge are useful reference material, but the rebuilt daemon needs Rust Ymir beside it as the authoritative physics library/service.
|
||||
|
||||
## Rebuild Doctrine
|
||||
|
||||
1. Document the current C# daemon API and client behavior.
|
||||
2. Measure it as the deprecated compatibility reference.
|
||||
3. Build the daemon and client semantics in the shape we actually want, then hoist reusable abstractions into CultMesh/CultLib instead of trapping them in Aetheria glue.
|
||||
4. Let Unity, TS, browser, native, Eve/CultUI, Rust, and future runtimes consume the same typed Verse semantics.
|
||||
5. Use this rebuild to make CultMesh expansive: cozy managed developer experience on top, very fast cross-runtime primitives underneath.
|
||||
|
||||
Compatibility means the new cross-runtime surface can understand enough of the old contract to migrate clients and compare intentional behavior. It does not mean the new architecture inherits old seams, and it does not mean client APIs keep their current awkward shape.
|
||||
|
||||
The test for a good abstraction is whether it belongs to Aetheria or to CultMesh. If a pattern is useful to any daemon with typed state, reactive UI surfaces, native view sharing, authority routing, or colocated query execution, it should move up into the shared cross-runtime library layer.
|
||||
|
||||
The concrete shared-layer target is tracked in `Aetheria.State/docs/cultmesh-cross-runtime-primitives.md`. Treat that note as the CultMesh/CultLib primitive checklist for this rebuild: Aetheria proves the shape, but the reusable machinery belongs in the cross-runtime library layer.
|
||||
|
||||
## Current C# Shape
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Host["Program.cs\nprocess host, CLI, tick loop, seeding"]
|
||||
Node["AetheriaStateNode\nCultMesh/CultCache/CultNet wrapper"]
|
||||
Commands["Daemon command docs\nEve command docs"]
|
||||
Authority["Authority router\npolicy + leases"]
|
||||
Tick["Tick runner"]
|
||||
Ops["Operations executor\nmutates run checkpoint"]
|
||||
Sim["RTS sim\nmovement, AI, combat, contacts"]
|
||||
Frame["Frame document\nrun snapshot + facts"]
|
||||
Projection["Projection helpers\nviewport, gravity, inventory"]
|
||||
Publications["Publications\nCultNet records, .cc files, RUDP snapshots, Eve surfaces"]
|
||||
Unity["Unity client\nobserver + SoA mmap"]
|
||||
TS["Electron/TS client\nCultMesh send, local reads"]
|
||||
|
||||
Host --> Node
|
||||
Node --> Commands
|
||||
Commands --> Authority
|
||||
Authority --> Tick
|
||||
Tick --> Ops
|
||||
Ops --> Sim
|
||||
Sim --> Frame
|
||||
Frame --> Projection
|
||||
Frame --> Publications
|
||||
Projection --> Publications
|
||||
Publications --> Unity
|
||||
Publications --> TS
|
||||
```
|
||||
|
||||
The current daemon is one C# executable around a CultMesh node. It ensures seed documents, reads typed command documents, authorizes them, applies operations, steps a compact RTS sim, publishes a new frame, and emits several secondary publications for clients.
|
||||
|
||||
That shape proved the core idea, but it also leaked implementation details into every edge:
|
||||
|
||||
- Unity observes daemon state through a C# facade and memory-mapped SoA view.
|
||||
- Electron sends CultMesh commands but still reads local `.cc` publications.
|
||||
- TS duplicates projection logic by decoding MessagePack arrays and slot constants.
|
||||
- RUDP snapshot handling injects ad hoc viewport projection records.
|
||||
- Eve/CultUI surface generation is mixed into daemon publication.
|
||||
- The host loop seeds scenario content and durable docs directly.
|
||||
- The API is typed in places, but not magical enough: clients still know too much about transport, record keys, snapshots, and projection plumbing.
|
||||
|
||||
## Measured C# API Surface
|
||||
|
||||
These files define the deprecated reference surface. They should be treated as API documentation and shakedown evidence first, not as design inspiration or strict parity law.
|
||||
|
||||
| Area | Source | What to freeze |
|
||||
| --- | --- | --- |
|
||||
| Host behavior | `Aetheria.State.Daemon/Program.cs` | CLI options, startup behavior, seed behavior, tick cadence, RUDP endpoint behavior, remote fact import behavior. |
|
||||
| CultMesh node wrapper | `Aetheria.State/AetheriaStateNode.cs` | Current record reads/writes, observed command query behavior, flush semantics. |
|
||||
| Registry | `Aetheria.State/AetheriaDocumentRegistry.cs` | C# document set currently exposed through CultCache/CultNet. |
|
||||
| Replica sync | `Aetheria.State/AetheriaVerseReplica.cs` | Scoped snapshot fetch, document fetch, remote committed fact import dependency. |
|
||||
| Local publications | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCultCacheDocumentStore.cs` | `.cc` compatibility files and their payload layout. |
|
||||
| Daemon documents | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs` | Schema ids, command enum, frame, command, committed fact, viewport, health, provider, command boundary, surface docs. |
|
||||
| Snapshot documents | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs` | Run, zone, entity, body, equipment, cargo, loadout, stat grids. |
|
||||
| Authority documents | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs` | Policy, rules, leases, modes, roles, deployment vocabulary, current authorization decisions. |
|
||||
| SoA documents | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs` | SoA backends, sync modes, column ids, dirty ranges, render groups. |
|
||||
| Command application | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs` | Current meaning of every gameplay command and rejection. |
|
||||
| Tick composition | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs` | Current order of command filtering, operation execution, sim step, frame creation, publication payloads. |
|
||||
| Compact sim | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsSimulation.cs` | Existing movement, hostile AI, combat, heat, and contact rules. |
|
||||
| Projections | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs` | Existing map, object, gravity, selected object, docking, refit, sector, and inventory projections. |
|
||||
| Client facade | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs` | C# client observation and typed operation ergonomics. |
|
||||
| Verse client | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs` | Current lower-level typed document reads/watches and command submission. |
|
||||
| Operation builders | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationClient.cs` and `AetheriaRuntimeDaemonOperationsClient.cs` | Existing typed operation vocabulary. |
|
||||
| Eve bridge | `Aetheria.State/AetheriaEveCommandBridge.cs` | Existing Eve command acceptance behavior for settings, catalog, policies, and surfaces. |
|
||||
| Unity consumption | `Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs` | Current Unity daemon observation, SoA remapping, and render native view handoff. |
|
||||
| TS consumption | `Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts`, `aetheria-rts-local-projection.ts`, `aetheria-local-publication-reader.ts` | Current Electron command sending, local publication reading, and duplicated projection behavior. |
|
||||
| Ymir C# contracts | `Assets/Scripts/ServerShared/YmirPhysicsContracts.cs` | Current body/world/query DTOs plus reference implementation for step, overlap, and cast queries. |
|
||||
| Ymir Unity bridge | `Assets/Scripts/Gameplay/Physics/AetheriaYmirPhysicsBridge.cs` | Current Unity presentation adapter that posts JSON to Ymir endpoints and maps daemon SoA bodies into Ymir query worlds. |
|
||||
| Ymir query tests | `Assets/Scripts/Tests/YmirPhysicsQueryTests.cs` | Current expectations for integration, radial fields, contacts, overlap sphere/circle, and cast sphere/circle; useful semantics should survive, DTO/endpoint shape should not. |
|
||||
|
||||
## Current Control Flow
|
||||
|
||||
The C# host does this today:
|
||||
|
||||
1. Parse `AetheriaDaemonHostOptions`.
|
||||
2. Open `AetheriaStateNode` with a CultMesh node, CultCache registry, and CultNet registry.
|
||||
3. Ensure baseline world, trade policy, playable run, Starbridge scenario/session, verse host settings, and authority policy.
|
||||
4. Publish runtime session and Eve surfaces.
|
||||
5. Start a loopback CultNet RUDP server for RTS clients.
|
||||
6. Tick immediately, then tick by interval:
|
||||
- Accept Eve commands.
|
||||
- Choose next frame id and simulation time.
|
||||
- Resolve current run from previous frame or durable documents.
|
||||
- Read loadout templates, daemon commands, authority policy, Starbridge state, and leases.
|
||||
- Filter commands through authority rules.
|
||||
- Execute daemon operations.
|
||||
- Step the compact RTS sim.
|
||||
- Build a daemon frame.
|
||||
- Publish committed command facts.
|
||||
- Import remote committed command facts.
|
||||
- Publish frame, health, command boundary, provider docs, Starbridge summary, SoA view, and Eve surfaces.
|
||||
|
||||
This is behavior to measure and learn from. It is not the desired architecture.
|
||||
|
||||
## Current Public Data Contract
|
||||
|
||||
The Rust daemon must be able to read or bridge these old records during deprecation:
|
||||
|
||||
| Record | Current key |
|
||||
| --- | --- |
|
||||
| Provider advertisement | `daemon:aetheria.provider_advertisement.v1` |
|
||||
| Health | `daemon:aetheria.health.v1` |
|
||||
| Command boundary | `daemon:aetheria.command_boundary.v1` |
|
||||
| Latest frame | `daemon:aetheria.frame.latest.v1` |
|
||||
| Latest SoA view | `daemon:aetheria.soa_view.latest.v1` |
|
||||
| Starbridge scenario | `starbridge:aetheria.scenario.latest.v1` |
|
||||
| Starbridge session | `starbridge:aetheria.session.latest.v1` |
|
||||
| Game Eve surface | `eve:surface:aetheria.daemon.game` |
|
||||
| Game Eve TUI surface | `eve:surface:aetheria.daemon.game.tui` |
|
||||
| Editor Eve surface | `eve:surface:aetheria.daemon.editor` |
|
||||
| Editor Eve TUI surface | `eve:surface:aetheria.daemon.editor.tui` |
|
||||
| Daemon command | `daemon:commands:{stable-command-id}:gamecult.aetheria.daemon_command.v1` |
|
||||
| Eve command | `eve:commands:{stable-command-id}:gamecult.eve.command.v1` |
|
||||
|
||||
Current schema roots:
|
||||
|
||||
| Contract | Source |
|
||||
| --- | --- |
|
||||
| Daemon command/frame/fact/projection/provider/health/boundary/surface docs | `AetheriaRuntimeDaemonDocuments.cs` |
|
||||
| Runtime run/zone/entity/body/equipment/cargo/loadout docs | `AetheriaRuntimeSnapshotDocuments.cs` |
|
||||
| SoA view docs and columns | `AetheriaRuntimeDaemonSoaDocuments.cs` |
|
||||
| Authority policies and leases | `AetheriaRuntimeVerseAuthorityPolicy.cs` |
|
||||
| Eve command docs | `AetheriaRuntimeEveCommandDocument.cs` |
|
||||
|
||||
## Current Gameplay Command Vocabulary
|
||||
|
||||
These are the gameplay operations exposed by the C# command document. The Rust rebuild should re-express them as typed native operations, not as public stringly transport calls.
|
||||
|
||||
| Area | Commands |
|
||||
| --- | --- |
|
||||
| Targeting | `SetTarget`, `ClearTarget`, `TargetNearest`, `TargetNext`, `TargetPrevious`, `TargetReticle` |
|
||||
| Pilot control | `SetMoveVector`, `SetLookDirection`, `SetTractorPower` |
|
||||
| Combat and equipment | `FireWeaponGroup`, `SetWeaponGroupActive`, `SetWeaponGroupMembership`, `SetBehaviorActive`, `ActivateConsumable`, `SensorPing`, `SetHeatsinksEnabled`, `SetOverrideShutdown`, `SetShutdownPerformance`, `SetItemEnabled`, `ToggleShieldEnabled`, `SetItemOverrideShutdown`, `SetThermotoggleTargetTemperature` |
|
||||
| Inventory, refit, economy | `TransferCargoItem`, `EquipItem`, `StoreItem`, `PickUpLoot`, `TradePurchase`, `RestoreLoadout`, `SetDockedCurrentShip` |
|
||||
| Interaction and travel | `Dock`, `DockNearest`, `Undock`, `Interact`, `EnterWormhole`, `TowToStation` |
|
||||
| Metadata and destruction | `SetEntityName`, `DestroyEntity`, `ToggleHullConductivity` |
|
||||
|
||||
## Rust Daemon Perfect Shape
|
||||
|
||||
The Rust daemon should be designed around typed state, typed operations, reactive subscriptions, and a Rust-native Ymir physics body.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Verse["CultMesh Verse\nschema, authority, replication"]
|
||||
State["Aetheria State\nWorld, zones, entities, inventory, stats"]
|
||||
Ops["Typed Operations\npilot, RTS, inventory, economy, editor"]
|
||||
Ymir["Ymir Rust\nphysics body, queries, spatial acceleration"]
|
||||
Sim["Aetheria Simulation\ncombat, heat, AI, sensors"]
|
||||
Queries["Typed Queries\nviewports, gravity, selection, inventory, stats"]
|
||||
Views["Native Views\nSoA slabs, WASM views, render views"]
|
||||
Surfaces["Eve/CultUI Surfaces\nstate pointers + typed commands"]
|
||||
Unity["Unity renderer/input shell"]
|
||||
Browser["Browser/RTS client"]
|
||||
Native["Native tools and daemons"]
|
||||
|
||||
Verse --> State
|
||||
Ops --> State
|
||||
State --> Ymir
|
||||
Ymir --> Sim
|
||||
Sim --> State
|
||||
State --> Queries
|
||||
Queries --> Views
|
||||
State --> Surfaces
|
||||
Views --> Unity
|
||||
Views --> Browser
|
||||
Views --> Native
|
||||
Surfaces --> Unity
|
||||
Surfaces --> Browser
|
||||
Surfaces --> Native
|
||||
```
|
||||
|
||||
Desired properties:
|
||||
|
||||
- A client can ask for typed state or a typed projection without knowing record-key plumbing.
|
||||
- A client can submit a typed operation without constructing transport payloads.
|
||||
- A UI surface can point at daemon state and let the CultUI runtime resolve it automatically.
|
||||
- Unity receives native render views and typed state handles, not gameplay ownership.
|
||||
- Browser clients can run as observers, controllers, or simulation hosts depending on Verse authority configuration.
|
||||
- The daemon owns simulation and projections as state-native behavior, not as publication side effects.
|
||||
- Ymir owns physical truth in Rust: stepping, overlaps, casts, contacts, broadphase, and spatial query acceleration.
|
||||
- CultMesh authority is explicit per Verse, with server-authoritative, trusted distributed, lease, quorum, and browser/WASM host structures remaining open.
|
||||
|
||||
## Ymir Rust Body
|
||||
|
||||
Ymir should be rebuilt as a Rust-native physics foundation used by Aetheria, Unity, browser/WASM, and future runtimes. Its current C# implementation is behavior evidence, not the long-term body and not strict parity law.
|
||||
|
||||
Current Ymir shape:
|
||||
|
||||
| Area | Current source | Notes |
|
||||
| --- | --- | --- |
|
||||
| Contracts and reference implementation | `Assets/Scripts/ServerShared/YmirPhysicsContracts.cs` | Defines `YmirWorld`, `YmirPhysicsBody`, radial fields, contacts, 2D circle queries, 3D sphere queries, and reference step/query math. |
|
||||
| Unity adapter | `Assets/Scripts/Gameplay/Physics/AetheriaYmirPhysicsBridge.cs` | Converts Unity/daemon render views into Ymir request DTOs and posts JSON to local query endpoints. This is presentation glue, not future architecture. |
|
||||
| Tests | `Assets/Scripts/Tests/YmirPhysicsQueryTests.cs` | Current checks for stepping, radial fields, overlap/cast ordering, contact output, and invalid input handling. |
|
||||
|
||||
The initial freeze exporter now emits `ymir-queries` probes from the current C# implementation: step integration, radial field acceleration, contact separation, overlap circle/sphere sorting, cast circle/sphere hits, and invalid-input behavior. These are typed measurement probes with explicit vector, world, body, field, hit, and contact records in JSON plus MessagePack. The next version should expand them with CultMath-shaped rect, circle, sphere, broadphase, sparse-cluster, and viewport-intersection fixtures.
|
||||
|
||||
Rust Ymir should provide:
|
||||
|
||||
- canonical `Vec2`, `Vec3`, `Rect`, `Circle`, `Sphere`, and body handles from CultMath-compatible primitives
|
||||
- deterministic step for Aetheria body motion
|
||||
- contacts and collision response for gameplay authority
|
||||
- `overlap_circle`, `cast_circle`, `overlap_sphere`, `cast_sphere`
|
||||
- viewport/intersection queries for gravity brushes and render/query visibility
|
||||
- sparse-cluster-friendly broadphase and spatial acceleration
|
||||
- SoA-friendly body storage that can be viewed by Aetheria and render clients
|
||||
- WASM-compatible query and step surface
|
||||
- CultMesh-friendly typed query/operation bindings where a runtime can ask for physical facts without knowing transport details
|
||||
|
||||
The C# JSON endpoint shape should be understood only for migration and comparison. The Rust API should feel native:
|
||||
|
||||
```rust
|
||||
let hits = ymir
|
||||
.world(zone_id)
|
||||
.overlap_sphere(Sphere::new(center, radius))
|
||||
.filter(FactionMask::hostile_to(actor))
|
||||
.collect();
|
||||
```
|
||||
|
||||
That is the bar. Unity can keep a bridge for click affordances and presentation, but physical truth belongs to Rust Ymir and is consumed by Aetheria through typed state/query handles.
|
||||
|
||||
## CultMesh Ergonomic Lessons
|
||||
|
||||
See `Aetheria.State/docs/cultmesh-cross-runtime-primitives.md` for the expanded primitive design, ownership boundary, code-shape examples, and performance contract. This section is the short form kept inside the daemon map.
|
||||
|
||||
Aetheria exposed where CultMesh should become more magical:
|
||||
|
||||
| Current leak | Better CultMesh shape |
|
||||
| --- | --- |
|
||||
| Clients manually know record keys. | CultMesh provides generated typed document handles and query handles. |
|
||||
| TS decodes MessagePack arrays by slot constants. | CultMesh provides generated typed TS/Rust/C#/WASM bindings with transparent encode/decode. |
|
||||
| Commands are documents pushed to known records. | CultMesh provides typed Verse operations with transport chosen by runtime. |
|
||||
| Local `.cc` files are used as client read path. | CultMesh subscriptions and scoped queries are the read path. |
|
||||
| Viewport projections are injected into snapshot responses. | CultMesh supports first-class typed query surfaces for derived state. |
|
||||
| Eve surfaces require manual state-ref resolution. | CultUI/CultMesh surface props can hold typed state pointers resolved by the runtime. |
|
||||
| Unity needs special observer glue. | CultMesh gives Unity the same typed state handles plus native render views. |
|
||||
| SoA view sharing is daemon-specific. | CultCache/CultMesh exposes typed native slices where co-location allows it. |
|
||||
| Authority logic is app-specific glue. | CultMesh exposes reusable authority policy, lease, claim, and runtime-role primitives. |
|
||||
| Colocated services still use bespoke endpoints. | CultMesh routes typed local calls, remote calls, WASM calls, and shared-memory/native-slice access behind one semantic API. |
|
||||
|
||||
This rebuild should feed back into CultMesh itself. If Aetheria needs a hand-written adapter to do an obvious typed-state thing, that is probably a missing CultMesh primitive. The target is managed and cozy for the developer, but not soft underneath: the runtime should still pick the fastest available path, whether that is an in-process view, a shared CultCache slab, a WASM view, or a remote Verse route.
|
||||
|
||||
### CultMesh Primitives To Hoist
|
||||
|
||||
These are not Aetheria conveniences. They are shared library capabilities that should make future daemons feel native from every runtime:
|
||||
|
||||
| Primitive | Shared capability |
|
||||
| --- | --- |
|
||||
| Typed document handles | Generated handles for records, schema ids, keys, subscriptions, and mutations. |
|
||||
| Typed operation handles | Method-shaped operations with generated payloads, authority metadata, routing, idempotency, and acknowledgements. |
|
||||
| Typed query surfaces | Composable derived-state queries that can run colocated, remote, or WASM without changing client code. |
|
||||
| Reactive state pointers | Values that UI surfaces can hold directly, with automatic resolution, invalidation, and subscription lifetimes. |
|
||||
| Native slice views | Safe typed views over CultCache/SoA slabs for Unity jobs, Rust, native clients, and WASM memory where possible. |
|
||||
| Authority primitives | Reusable policy modes, leases, claims, runtime roles, and future quorum/consensus hooks. |
|
||||
| Geometry/math values | Shared `Vec2`, `Vec3`, `Rect`, `Circle`, `Sphere`, transforms, and deterministic scalar helpers. |
|
||||
| Locality-aware routing | Runtime chooses shared-memory, in-process, IPC, network, or WASM transport while the API remains semantic. |
|
||||
| Schema evolution | Generated migration manifests, compatibility readers, and versioned operation/query contracts. |
|
||||
| Surface bindings | Eve/CultUI components bind to typed state handles and operation handles instead of string refs and command names. |
|
||||
|
||||
### Sugar Targets
|
||||
|
||||
These examples are aspirational API targets. They are not promises about exact names; they show how little transport code a client should need to express ordinary Aetheria work.
|
||||
|
||||
Current TS RTS client command path:
|
||||
|
||||
```ts
|
||||
const commandId = crypto.randomUUID();
|
||||
const issuedAtUtc = new Date().toISOString();
|
||||
await client.sendCommandDocument(
|
||||
commandId,
|
||||
issuedAtUtc,
|
||||
encodeSetMoveVectorCommand(commandId, issuedAtUtc, runtimeId, {
|
||||
actorEntityKey,
|
||||
directionX,
|
||||
directionY,
|
||||
scalarValue,
|
||||
}));
|
||||
```
|
||||
|
||||
Desired TS CultMesh operation sugar:
|
||||
|
||||
```ts
|
||||
await verse
|
||||
.aetheria()
|
||||
.entity(actor)
|
||||
.pilot()
|
||||
.move({ direction: vec2(x, y), throttle });
|
||||
```
|
||||
|
||||
Current TS projection path:
|
||||
|
||||
```ts
|
||||
const frame = await publications.readDaemonFrame();
|
||||
const objects = projectObjectsViewportFromFrame(frame, {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
controlledEntityIndices,
|
||||
});
|
||||
```
|
||||
|
||||
Desired TS query sugar:
|
||||
|
||||
```ts
|
||||
const objects = await verse
|
||||
.aetheria()
|
||||
.zone(zoneId)
|
||||
.objects()
|
||||
.visibleTo(controlledUnits)
|
||||
.within(rect.xy(min, max))
|
||||
.watch();
|
||||
```
|
||||
|
||||
Current Unity observation path:
|
||||
|
||||
```csharp
|
||||
var observed = ResolveClient().ObserveAsync().GetAwaiter().GetResult();
|
||||
if (observed != null && observed.HasSoaView)
|
||||
{
|
||||
AetheriaDaemonSoaMemoryMap.TryOpen(observed.SoaIndex, out var map, out _);
|
||||
AetheriaDaemonRenderNativeView.TryCreate(observed.SoaIndex, map, out var view);
|
||||
}
|
||||
```
|
||||
|
||||
Desired Unity renderer sugar:
|
||||
|
||||
```csharp
|
||||
await foreach (var view in verse
|
||||
.Aetheria()
|
||||
.CurrentZone()
|
||||
.RenderView()
|
||||
.AsNativeArrays())
|
||||
{
|
||||
ShipRenderJobs.Schedule(view.Entities);
|
||||
}
|
||||
```
|
||||
|
||||
Current Ymir query bridge shape:
|
||||
|
||||
```csharp
|
||||
var request = new YmirCircleOverlapRequest
|
||||
{
|
||||
world = world,
|
||||
center = ToVec2(center),
|
||||
radius = radius
|
||||
};
|
||||
var hits = PostJson<YmirCircleOverlapRequest, YmirCircleOverlapResult>(OverlapCircleUrl, request).hits;
|
||||
```
|
||||
|
||||
Desired Rust Ymir/Aetheria query sugar:
|
||||
|
||||
```rust
|
||||
let hits = verse
|
||||
.aetheria()
|
||||
.zone(zone)
|
||||
.physics()
|
||||
.overlap_circle(Circle::new(center, radius))
|
||||
.exclude(actor)
|
||||
.collect()
|
||||
.await?;
|
||||
```
|
||||
|
||||
Current Eve surface state-ref resolution:
|
||||
|
||||
```csharp
|
||||
var resolver = AetheriaRuntimeVerseClient.CreateEveSurfaceStateRefResolver(statePath, runtimeId);
|
||||
var label = resolver("aetheria.daemon/frame/currentEntity/name");
|
||||
```
|
||||
|
||||
Desired CultUI state pointer sugar:
|
||||
|
||||
```ts
|
||||
surface.label({
|
||||
text: verse.aetheria().currentEntity().name,
|
||||
});
|
||||
|
||||
surface.button({
|
||||
icon: "crosshair",
|
||||
command: verse.aetheria().currentEntity().targetNearest,
|
||||
});
|
||||
```
|
||||
|
||||
Current C# operation facade is close, but still hides snapshot/transport setup behind a Unity-owned observer:
|
||||
|
||||
```csharp
|
||||
observer.Operations.SetTarget(targetEntityKey);
|
||||
observer.Operations.FireWeaponGroup(0);
|
||||
```
|
||||
|
||||
Desired cross-runtime typed operation sugar:
|
||||
|
||||
```csharp
|
||||
await verse.Aetheria()
|
||||
.Entity(actor)
|
||||
.Combat
|
||||
.Target(target)
|
||||
.Fire(group: 0);
|
||||
```
|
||||
|
||||
Current refit/economy fixture shape:
|
||||
|
||||
```csharp
|
||||
command.TextValue = "repair-parts";
|
||||
command.CargoTransfer.OriginEntityKey = station;
|
||||
command.CargoTransfer.OriginCargoIndex = 0;
|
||||
command.CargoTransfer.DestinationEntityKey = actor;
|
||||
command.CargoTransfer.DestinationCargoIndex = 0;
|
||||
command.CargoTransfer.SourceX = 0;
|
||||
command.CargoTransfer.SourceY = 0;
|
||||
command.CargoTransfer.DestinationX = 2;
|
||||
command.CargoTransfer.DestinationY = 0;
|
||||
command.CargoTransfer.HasDestinationPosition = true;
|
||||
```
|
||||
|
||||
Desired inventory operation sugar:
|
||||
|
||||
```ts
|
||||
await verse
|
||||
.aetheria()
|
||||
.entity(actor)
|
||||
.inventory()
|
||||
.cargo()
|
||||
.takeFrom(station.inventory().cargo())
|
||||
.item("repair-parts")
|
||||
.placeAt(grid(2, 0));
|
||||
```
|
||||
|
||||
Desired trade sugar:
|
||||
|
||||
```rust
|
||||
verse
|
||||
.aetheria()
|
||||
.entity(actor)
|
||||
.inventory()
|
||||
.buy_from(station)
|
||||
.item("reactor-fuel")
|
||||
.quantity(1)
|
||||
.place_in(CargoBay::primary())
|
||||
.await?;
|
||||
```
|
||||
|
||||
The common pattern: state access starts from a typed Verse handle, operations hang from typed state objects, queries compose like native collections, and subscriptions return reactive state or native views. Record keys, schema ids, endpoint selection, MessagePack slots, command ids, and local/remote routing are CultMesh runtime concerns.
|
||||
|
||||
## Deprecation Boundary
|
||||
|
||||
The C# daemon becomes deprecated when these artifacts exist:
|
||||
|
||||
1. A written measurement snapshot of current schemas, record keys, operation meanings, client semantics, and projection outputs.
|
||||
2. Migration probe fixtures for:
|
||||
- seeded run state
|
||||
- command batches
|
||||
- authority acceptance/rejection
|
||||
- post-tick frames
|
||||
- objects viewport
|
||||
- gravity viewport
|
||||
- selected object
|
||||
- inventory
|
||||
- SoA view descriptor
|
||||
3. A Rust daemon design that does not inherit local publication files, TS projection duplication, Unity coordinate leakage, or host-loop scenario seeding.
|
||||
4. A CultMesh ergonomic checklist identifying shared cross-runtime primitives to hoist before the daemon and clients harden around local adapters. The detailed checklist lives in `Aetheria.State/docs/cultmesh-cross-runtime-primitives.md`.
|
||||
|
||||
After that, the C# daemon should be deprecated except for critical migration fixes.
|
||||
|
||||
The concrete artifact and fixture checklist lives in `Aetheria.State/docs/rust-rebuild-freeze-checklist.md`.
|
||||
|
||||
## Rust Rebuild Modules
|
||||
|
||||
These are product-shape modules, not incremental migration slices:
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `aetheria_schema` | Canonical schemas, generated bindings, record/query/operation identities, and legacy MessagePack migration support. |
|
||||
| `aetheria_world` | World, zones, entities, bodies, inventory, loadouts, stats, keys, deterministic state layout. |
|
||||
| `aetheria_authority` | Verse authority policies, leases, roles, claims, and runtime participation rules. |
|
||||
| `aetheria_ops` | Typed gameplay operations and validation. |
|
||||
| `ymir_math` | CultMath-compatible vectors, rects, circles, spheres, transforms, deterministic scalar helpers. |
|
||||
| `ymir_physics` | Rust-native bodies, fields, broadphase, stepping, contacts, overlap/cast queries, spatial acceleration. |
|
||||
| `aetheria_sim` | Aetheria rules over Ymir physics: combat, heat, AI, sensors, stat evaluation, deterministic tick logic. |
|
||||
| `aetheria_query` | First-class typed queries for objects viewport, gravity viewport, current object, inventory, stats, Starbridge state, and client-visible sets. |
|
||||
| `aetheria_views` | SoA/native/WASM render views, dirty ranges, and cross-runtime native slice semantics. |
|
||||
| `aetheria_surfaces` | Eve/CultUI state-backed surfaces, state pointer resolution, and operation bindings. |
|
||||
| `aetheria_host` | Native daemon process, CultMesh runtime, persistence, authority mode configuration. |
|
||||
| `aetheria_wasm` | Browser simulation host and query runtime. |
|
||||
|
||||
The module boundary should stay honest: Aetheria modules define Aetheria concepts, while CultMesh/CultLib absorbs the general machinery for typed state, generated handles, authority routing, native views, geometry values, reactive surfaces, transport selection, and schema evolution.
|
||||
|
||||
## Design Constraints For The New Daemon
|
||||
|
||||
- Gameplay plane is XY. Unity XZ conversion belongs in Unity adapters.
|
||||
- State schema should model vectors and rects as first-class values.
|
||||
- Ymir primitives should be Rust-first and CultMath-compatible; C# DTOs are compatibility mirrors.
|
||||
- SoA columns may store vector values as vector columns; they do not need to split every component unless the query actually benefits.
|
||||
- The simulation receives explicit tick time. Wall clock is publication metadata only.
|
||||
- Queries are derived state. Clients should ask for what they need: visible objects, gravity influences, stats, inventory, render views.
|
||||
- Rendering views are not authority. Unity, browser, and native clients consume views and submit typed operations.
|
||||
- The browser can be a simulation host when authority policy grants it that role.
|
||||
- Local co-deployment can expose native slices, but remote runtimes must see the same semantic state through CultMesh.
|
||||
- The new API should make the common path short enough that client code reads like native Aetheria code.
|
||||
|
||||
## Things To Bury With The C# Daemon
|
||||
|
||||
- Local `.cc` publication files as normal client API.
|
||||
- C# or Unity-shaped Ymir as the authority path.
|
||||
- JSON HTTP Ymir calls as the normal in-process/co-deployed path.
|
||||
- TS projection copies that parse raw MessagePack slots.
|
||||
- RUDP snapshot projection injection as the query model.
|
||||
- Unity coordinate debt in shared schema.
|
||||
- Host-loop seeding as world definition.
|
||||
- UI surfaces that require external manual state-ref resolution.
|
||||
- Client code that knows transport boundaries for ordinary state access.
|
||||
- Any public `Apply(command, payload)` style operation surface.
|
||||
|
||||
## First Rebuild Milestone
|
||||
|
||||
The first Rust milestone should demonstrate the final shape, not a half-port:
|
||||
|
||||
1. Define canonical schema and hoisted CultMesh primitives for Rust, TS, Unity/C#, browser/WASM, and Eve/CultUI, using `Aetheria.State/docs/cultmesh-cross-runtime-primitives.md` as the shared-layer target.
|
||||
2. Launch a Rust Aetheria Verse node.
|
||||
3. Load the measured C# fixture world as a migration sample.
|
||||
4. Accept typed operations from a TS client through CultMesh.
|
||||
5. Run simulation authority in Rust.
|
||||
6. Answer typed viewport and gravity queries.
|
||||
7. Publish a native/WASM-friendly render view.
|
||||
8. Let a browser client render and command the world without local `.cc` reads or duplicated projection code.
|
||||
9. Keep Unity as an optional renderer/input shell over the same typed state and render view semantics.
|
||||
|
||||
That is the bar: C# is documented, measured, and deprecated; the rebuild is not the old daemon in a new language, it is the daemon, client API, and CultMesh surface Aetheria was trying to become.
|
||||
105
Aetheria.State/docs/rust-rebuild-freeze-checklist.md
Normal file
105
Aetheria.State/docs/rust-rebuild-freeze-checklist.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Aetheria Rust Rebuild Freeze Checklist
|
||||
|
||||
This checklist turns the current C# daemon and client integrations into a measured, deprecated reference before the rebuild begins in earnest. The goal is not to migrate the C# shape piece by piece, and it is not strict parity with every current wart. The goal is to make current behavior visible enough that the new daemon, client APIs, and shared CultMesh/CultLib primitives can deliberately preserve, improve, or discard it while building the clean Aetheria/Ymir/CultMesh shape.
|
||||
|
||||
## Freeze Artifacts
|
||||
|
||||
| Artifact | Source of truth | Output |
|
||||
| --- | --- | --- |
|
||||
| Daemon schema catalog | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs` | Generated schema manifest with schema ids, document names, MessagePack key order, enum ids, and record key conventions. |
|
||||
| Runtime snapshot schema catalog | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs` | Generated schema manifest for run, zone, entity, body, inventory, loadout, cargo, equipment, and stat grids. |
|
||||
| Authority schema catalog | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs` | Generated schema manifest plus policy-mode behavior table. |
|
||||
| Ymir C# reference sample | `Assets/Scripts/ServerShared/YmirPhysicsContracts.cs` | Current DTO and query behavior evidence for Rust Ymir design. Preserve only semantics we still want. |
|
||||
| Current TS generated contract | `Aetheria.Rts.Web/scripts/generate-rts-bindings.mjs` and `Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts` | Evidence that schema extraction exists today; replace it with shared CultMesh/CultLib generation for typed handles, operations, queries, state pointers, native views, authority, and migration manifests. |
|
||||
| Shared CultMesh/CultLib primitive target | `Aetheria.State/docs/cultmesh-cross-runtime-primitives.md` | Design bar and acceptance vocabulary for hoisting typed documents, operations, queries, state pointers, native views, authority, geometry, locality routing, schema evolution, and surface bindings out of Aetheria glue. |
|
||||
| Initial daemon fixture exporter | `Aetheria.State.Freeze/Program.cs` | Runs the C# daemon once, reads current daemon publications, emits normalized JSON plus MessagePack bytes for frame, public docs, SoA descriptor, projection fixtures, authority decisions, Ymir query probes, and seeded pilot/interaction/refit command-batch fixtures. |
|
||||
| Current client verifier gates | `Aetheria.Rts.Web/scripts/verify-stage7b-rts-client.ps1`, `verify-stage7c-local-runtime.ps1`, `verify-stage7c-electron-shell.ps1`, `Aetheria.State/scripts/verify-stage7d-unity-parity.ps1` | Current smoke gates while C# is still live. They prove today still runs; they do not define the future daemon or client API surface. |
|
||||
|
||||
## Golden Fixtures
|
||||
|
||||
The C# daemon and clients are measured when these fixtures can be generated from the current implementation. The rebuild should consume them as migration probes: green where current behavior is intentional, red or replaced where current behavior is accidental, and redesigned wherever the client semantics should become simpler.
|
||||
|
||||
| Fixture | Must contain | Proves |
|
||||
| --- | --- | --- |
|
||||
| `seeded-local-rts-world` | The daemon-seeded `local-rts` run with zones, entities, bodies, inventory, loadouts, stat grids, current entity, and authority policy. | Rust can load current world semantics without inheriting the host seeding code. |
|
||||
| `command-batch-pilot` | Targeting, movement, look, tractor, sensor ping, shield toggle, weapon group fire. Initial exporter emits this batch and captures applied/rejected command accounting. | Rust operations cover the same gameplay intents through typed operations. |
|
||||
| `command-batch-refit` | Rename, transfer cargo, trade purchase, equip from cargo, store equipped item, set docked current ship, pick up loot, restore loadout. Initial exporter emits this batch and captures applied/rejected command accounting: rename/transfer/trade/equip/store currently apply; current-ship selection, pickup without a seeded drop, and restore without a template currently reject. | Rust operations cover inventory/economy/refit intent without inheriting command-document ergonomics or accidental rejection behavior. |
|
||||
| `command-batch-interaction` | Dock, dock nearest, undock, interact, tow to station. Initial exporter emits this batch and captures applied/rejected command accounting. | Rust operations cover interaction/travel intent and may simplify ambiguous current behavior. |
|
||||
| `authority-decisions` | Allowed and rejected commands for host-authoritative, delegated runtime, owning runtime, and lease cases. | Rust authority has explicit behavior for currently implemented modes and explicit design choices for modes C# only stubs. |
|
||||
| `post-tick-frame` | Frame after a fixed command batch and fixed delta, including command accounting and committed facts. | Rust tick composition can be compared to current output where useful, then diverge where the new simulation model is better. |
|
||||
| `objects-viewport` | XY rect, controlled unit set, visible objects, statuses, inventory summaries. | Rust query surface can replace TS local projection copies. |
|
||||
| `gravity-viewport` | XY rect, intersecting gravity brushes, body views. | Rust query surface can replace Unity/TS gravity reconstruction. |
|
||||
| `selected-object` | Entity selection, status, target, faction, inventory identity. | Rust selected-object query supports current UI expectations through cleaner typed state. |
|
||||
| `inventory` | Equipment and cargo rows, source identity, item stats, quantity, quality, durability. | Rust inventory projection can drive Unity and RTS panels. |
|
||||
| `soa-view` | Current-zone render columns, column kinds, vector columns, dirty ranges, native view descriptor. | Rust view publication can feed Unity and native clients. |
|
||||
| `ymir-queries` | Step integration, radial field acceleration, contact separation, overlap circle, cast circle, overlap sphere, cast sphere, and invalid-input cases. Initial exporter emits typed request/result probes with explicit vector, world, body, field, hit, and contact records in JSON plus MessagePack. Vector fields now use shared CultLib `GameCult.Geometry` primitives (`CultVec2`/`CultVec3`) instead of fixture-local DTOs. | Rust Ymir preserves useful physical semantics while replacing C# endpoint shape with Rust/CultMath primitives. |
|
||||
|
||||
## Rust Rebuild Acceptance Gates
|
||||
|
||||
The first rebuild milestone is accepted only when the new architecture demonstrates the intended cross-runtime shape.
|
||||
|
||||
1. A Rust Verse node can load the measured fixture world and publish typed Aetheria state.
|
||||
2. A TS client can submit typed operations through shared CultMesh operation handles without constructing command documents, record keys, or raw MessagePack payloads.
|
||||
3. A browser client can render map state by watching shared typed object/gravity query surfaces, not by reading local `.cc` files or duplicating projection code.
|
||||
4. A Unity client can consume a shared typed render view as native arrays without owning gameplay state or running Unity physics for authority.
|
||||
5. Rust Ymir owns physical truth for step, overlap, cast, contact, and broadphase queries.
|
||||
6. Eve/CultUI surfaces can bind to shared CultMesh state pointers and typed operations without manual state-ref resolver glue in client code.
|
||||
7. Authority policy is configured through shared CultMesh authority primitives and changes who may author claims without changing operation schemas.
|
||||
8. The API can run in native daemon mode and expose a WASM-compatible surface for browser simulation host mode.
|
||||
9. The old C# daemon can be turned off for the browser RTS proof and the world still runs, renders, and accepts commands.
|
||||
|
||||
## CultMesh Sugar Gates
|
||||
|
||||
These are not cosmetic. If these fail, the architecture is still leaking.
|
||||
|
||||
The expanded primitive spec is `Aetheria.State/docs/cultmesh-cross-runtime-primitives.md`. Use it as the shared-library ownership boundary whenever a new adapter starts appearing in Aetheria-specific code.
|
||||
|
||||
| Gate | Bad smell | Desired shape |
|
||||
| --- | --- | --- |
|
||||
| Typed operations | Client code constructs `CommandId`, `recordKey`, `schemaId`, or `document_put_raw`. | CultMesh operation handles expose `verse.aetheria().entity(actor).pilot().move(...)`. |
|
||||
| Typed queries | Client code fetches a frame and calls local projection helpers. | CultMesh query surfaces expose `verse.aetheria().zone(zone).objects().visibleTo(units).within(rect)`. |
|
||||
| Typed state pointers | UI code resolves string state refs manually. | CultMesh state pointers are first-class values resolved by CultUI/CultMesh runtime. |
|
||||
| Native views | Unity code manually opens memory maps by daemon-specific paths. | CultMesh native slice views expose `RenderView().AsNativeArrays()` from the typed Verse handle. |
|
||||
| Authority | Each daemon invents policy, lease, claim, and runtime-role glue. | CultMesh authority primitives carry policy modes, claims, leases, runtime roles, and future quorum hooks. |
|
||||
| Geometry | Each runtime reinvents vectors, rects, circles, spheres, and coordinate conversions. | CultMath/CultMesh provide shared geometry values and deterministic scalar helpers. |
|
||||
| Locality | Co-deployed services still talk through bespoke URLs or files. | CultMesh chooses in-process, shared slab, IPC, network, or WASM transport behind one semantic API. |
|
||||
| Ymir queries | Unity or TS posts JSON to a hard-coded local URL for normal co-deployed queries. | Aetheria/Ymir queries are typed, colocated when possible, and remotely routable when needed. |
|
||||
| Schema generation | TS, C#, and Rust each hand-maintain slot maps. | One schema source generates cross-runtime CultMesh bindings and legacy migration manifests. |
|
||||
| Client semantics | Each runtime gets bespoke helper layers and transport assumptions. | Unity, TS, browser, Rust, native, and Eve/CultUI share the same operation/query/state semantics with runtime-specific ergonomic sugar. |
|
||||
|
||||
## Current Verification Commands
|
||||
|
||||
These are current-state gates, not proof that the Rust rebuild is complete. They are useful while freezing the C# contract.
|
||||
|
||||
```powershell
|
||||
dotnet build .\GameCult.Aetheria.State.Unity.csproj /clp:ErrorsOnly /p:UseSharedCompilation=false
|
||||
dotnet build .\Aetheria.State.Daemon\Aetheria.State.Daemon.csproj /clp:ErrorsOnly /p:UseSharedCompilation=false
|
||||
dotnet run --project .\Aetheria.State.Verify\Aetheria.State.Verify.csproj
|
||||
.\Aetheria.State\scripts\verify-stage7d-unity-parity.ps1
|
||||
cd .\Aetheria.Rts.Web
|
||||
npm run check:rts-bindings
|
||||
npm run build
|
||||
npm run verify:stage7b
|
||||
npm run verify:stage7c
|
||||
npm run verify:stage7c:electron
|
||||
```
|
||||
|
||||
## Immediate Build Work
|
||||
|
||||
1. Expand `Aetheria.State.Freeze` beyond baseline plus pilot/interaction/refit/authority/Ymir probes into post-tick, richer authority, and richer Ymir fixture families.
|
||||
2. Expand `generate-rts-bindings.mjs` or replace it with shared CultMesh/CultLib schema and binding generation for Rust, TS, Unity/C#, browser/WASM, and Eve/CultUI, including legacy migration manifests.
|
||||
3. Expand the initial CultMath-shaped Ymir fixture cases with explicit rect, circle, sphere, broadphase, sparse-cluster, and viewport-intersection schemas.
|
||||
4. Begin the shared primitive implementation from `cultmesh-cross-runtime-primitives.md` before the Rust daemon hardens around one-off Aetheria adapters.
|
||||
5. Create an initial Rust workspace with `ymir_math`, `ymir_physics`, `aetheria_schema`, and `aetheria_world`.
|
||||
6. Make Rust Ymir satisfy the intentional Ymir fixture cases before wiring it into Aetheria; document any deliberate improvements over C# behavior.
|
||||
7. Make Rust Aetheria load the measured world fixture and answer object/gravity viewport queries through hoisted CultMesh query/state primitives shared by TS, Unity, browser, and Eve/CultUI.
|
||||
|
||||
Generate the initial daemon fixture set with:
|
||||
|
||||
```powershell
|
||||
dotnet run --project .\Aetheria.State.Freeze\Aetheria.State.Freeze.csproj -- --root . --state .\obj\freeze\aetheria-freeze-state.cc --out .\Aetheria.State\fixtures\rust-rebuild-freeze
|
||||
```
|
||||
|
||||
For smoke runs that should not touch checked-in fixtures, write to `.\obj\freeze-smoke\fixtures`.
|
||||
|
||||
The rebuild begins once the C# behavior is measured enough to learn from it without copying its leaks.
|
||||
298
Aetheria.State/docs/stage-7-client-surface-inventory.md
Normal file
298
Aetheria.State/docs/stage-7-client-surface-inventory.md
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
# Stage 7A Client Surface Inventory
|
||||
|
||||
Date: 2026-06-24
|
||||
|
||||
Stage 7A exists to stop the client migration from turning into Jenga. This is
|
||||
the reviewed inventory for Unity, Electron, and daemon client-facing surfaces.
|
||||
Stage 7D.1 refreshed the Unity half of this ledger before Unity parity code
|
||||
movement began.
|
||||
|
||||
## Gate
|
||||
|
||||
Current gate: Stage 7D.4 Unity projection reroute.
|
||||
|
||||
Implementation runbook:
|
||||
|
||||
- `Aetheria.State/docs/stage-7-thin-client-staged-implementation-plan.md`
|
||||
|
||||
Stage 7A is complete. Stage 7D.1 reuses this artifact as the edit queue: every
|
||||
remaining Unity-facing mutation and gameplay read/projection path must be
|
||||
assigned to `7D.2 facade`, `7D.3 commands`, `7D.4 reads`, `7D.5 renderer`, or
|
||||
`8 demolition`. Stage 8 Unity gameplay shell demolition may not start until
|
||||
Stage 7E passes.
|
||||
|
||||
## Target Shape
|
||||
|
||||
```text
|
||||
client input -> local typed Verse command document -> local daemon
|
||||
local daemon state -> local typed projection/read -> renderer or UI panel
|
||||
peer daemon facts -> local authority import -> local state
|
||||
```
|
||||
|
||||
Forbidden as public client shape:
|
||||
|
||||
- `command(kind, payload)`;
|
||||
- browser or Unity code constructing transport payloads directly;
|
||||
- hand-encoded MessagePack array layouts in client code;
|
||||
- remote gameplay viewport reads used as state authority;
|
||||
- cached command-port facades or ad-hoc buses between input and the local Verse
|
||||
command document.
|
||||
|
||||
## Electron Mutation Surfaces
|
||||
|
||||
| Surface | Evidence | Current Shape | Decision | Stage |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Browser right-click move/target | `Aetheria.Rts.Web/Client/app.ts:478`, `:490` | Browser now calls typed `window.aetheriaRts.setTarget(...)` and `setMoveVector(...)`; it no longer constructs `{ kind: ... }` transport payloads. | Keep as temporary typed facade. Replace with shared generated TS Verse bindings when available. | 7B/7C |
|
||||
| Preload command bridge | `Aetheria.Rts.Web/Electron/preload.cjs:5`, `:6` | Exposes typed `setMoveVector` and `setTarget` IPC calls. | Keep until the broader typed runtime facade replaces preload method wiring. | 7B |
|
||||
| Electron IPC command handler | `Aetheria.Rts.Web/Electron/main.ts:98`, `:101` | Accepts `RtsSetMoveVectorRequest` and `RtsSetTargetRequest` and forwards to typed client methods. | Keep as Stage 7B shim. | 7B |
|
||||
| TypeScript CultMesh command writer | `Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts`; `Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts`; `Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts` | Transport wrapper exposes typed methods; generated TS contract metadata owns schema ids, command kind ids, and MessagePack slot maps derived from the C# `[Key]` declarations, including the current 30-slot daemon command document. | Keep the semantic encode helpers here until the broader CultMesh TS typed document writer lands; do not reintroduce manual slot maps. | 7B |
|
||||
| Daemon RTS command receive hook | `Aetheria.State.Daemon/Program.cs:454` | RUDP handler accepts raw command documents and submits them to the local node. | Quarantine for 7B, then replace or narrow. It can remain as an internal local typed document transport endpoint, but it must not define the public client API. | 7B |
|
||||
|
||||
## Electron Read And Projection Surfaces
|
||||
|
||||
| Surface | Evidence | Current Shape | Decision | Stage |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Browser viewport polling | `Aetheria.Rts.Web/Client/app.ts:166` | Browser requests `mapViewport` every 50 ms and treats the returned document as the main map projection. | Keep as named projection shim, but replace underlying remote viewport request with local typed projection reads. | 7B/7C |
|
||||
| Preload viewport bridge | `Aetheria.Rts.Web/Electron/preload.cjs:4` | Exposes typed `mapViewport` projection request. | Keep until the broader typed runtime facade replaces preload method wiring. | 7B |
|
||||
| Electron IPC viewport handler | `Aetheria.Rts.Web/Electron/main.ts:95` | Accepts typed viewport bounds and forwards to `AetheriaCultMeshClient.mapViewport`. | Keep as Stage 7B shim. | 7B |
|
||||
| TypeScript CultMesh viewport reader | `Aetheria.Rts.Web/Electron/aetheria-cultmesh.ts`; `Aetheria.Rts.Web/Electron/aetheria-rts-bindings.ts`; `Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts` | Transport wrapper asks for the named RTS viewport schema; generated TS contract metadata owns viewport slot maps while semantic decode helpers produce ergonomic UI types. | Replace the remote viewport request with local typed projection reads. | 7B/7C |
|
||||
| Daemon RTS viewport hook | `Aetheria.State.Daemon/Program.cs:420`, `:501`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs` | Daemon snapshot hook now delegates viewport construction to the shared typed `AetheriaRuntimeRtsProjection` API. The hook still serves it over RUDP for Electron. | Keep only as temporary transport plumbing. Replace Electron's RUDP viewport read with local projection reads, then delete or narrow this hook. | 7B/7C |
|
||||
|
||||
## Unity Mutation Surfaces
|
||||
|
||||
| Surface | Evidence | Current Shape | Decision | Stage |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Runtime input settings commands | `Assets/Scripts/UI/InputScreen/InputDisplayLayout.cs` | `InputDisplayLayout` now owns an explicit `AetheriaClient` and submits typed input settings commands through the shared facade. The old static `ActionGameManager` input-settings ingress has been deleted. | Keep as the Stage 7D direct-panel pattern: panel owns presentation/input state, facade owns typed Verse submission. | 7D |
|
||||
| Loadout restore | `Assets/Scripts/Gameplay/ActionGameManager.cs:1110`; caller at `Assets/Scripts/UI/Menu/InventoryPanel.cs:258` | Unity UI calls manager method for daemon-owned loadout operation. | Replace with typed client facade method. | 7D |
|
||||
| Daemon operation submitter | `Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs:40`, `:44`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationClient.cs:49`, `:54` | Unity uses typed operation wrappers that open `AetheriaRuntimeVerseClient` and submit `AetheriaRuntimeDaemonCommandDocument`. | Keep as the semantic source for Stage 7B typed commands, but remove per-call client open churn when the shared runtime wrapper lands. | 7B/7D |
|
||||
| Surface command helpers | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSurfaceCommands.cs:23`, `:37` | Eve/CultUI surface commands submit typed daemon operations. | Keep, but ensure surface command templates point at typed documents or typed state refs rather than generic payload maps. | 7D |
|
||||
|
||||
## Unity Read And Projection Surfaces
|
||||
|
||||
| Surface | Evidence | Current Shape | Decision | Stage |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Daemon observer polling | `Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs:130`, `:139` | Opens `AetheriaRuntimeVerseClient` and reads observed daemon state. | Keep. This is the closest current Unity surface to the target contract. | 7D |
|
||||
| SoA native view mapping | `Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs:83`, `:153` | Maps daemon SoA view into `AetheriaDaemonRenderNativeView`. | Keep. This remains the rendering fast path. | 7D/8 |
|
||||
| `ActionGameManager` frame lowering | `Assets/Scripts/Gameplay/ActionGameManager.cs`; `Assets/Scripts/Gameplay/AetheriaUnityObservedFrameApplier.cs` | Unity loop delegates `ApplyLatestZoneRender` to the observed frame applier, which reads the typed `ZoneRenderAsync()` facade. | Quarantine. Stage 7D may keep it as a shell; Stage 8 should delete or reduce it once Unity renders from projection/native views. | 7D/8 |
|
||||
| Observed Galaxy facade | `Assets/Scripts/Gameplay/AetheriaUnityObservedRunProjection.cs`; created from typed sector-map boot in `Assets/Scripts/UI/MainMenu.cs` | Unity maintains an observed facade graph only as a legacy scene-construction adapter. `ActionGameManager` no longer exposes or stores it. | Quarantine for Stage 7D, demolish in Stage 8 where clients can use equivalent typed projections. | 7D/8 |
|
||||
| `ZoneRenderer` frame projection | `Assets/Scripts/Zone Display/ZoneRenderer.cs:239` | Persistent GameObject projection cache lowers typed `zone_render`, viewport, and contact feeds. | Keep as presentation cache only until Stage 8. It must not become an authority or mirrored hierarchy owner. | 7D/8 |
|
||||
| Menu reads from manager singletons | Examples: `Assets/Scripts/UI/Menu/SectorMap.cs:94`, `Assets/Scripts/UI/Menu/InventoryMenu.cs:411`, `Assets/Scripts/UI/Menu/TradeMenu.cs:530` | UI panels read catalog/settings/state through `ActionGameManager`. | Replace with shared client projection/state refs as panels are migrated. | 7D/8 |
|
||||
|
||||
## Shared Runtime Contract Surfaces To Keep
|
||||
|
||||
| Surface | Evidence | Decision |
|
||||
| --- | --- | --- |
|
||||
| `AetheriaRuntimeVerseClient` | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs:153`, `:177`, `:508` | Keep and make it the shape both Unity and Electron mirror. TS needs generated/manual bindings for the same typed records. |
|
||||
| Typed operation wrappers | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationClient.cs:7`; `AetheriaRuntimeDaemonOperationsClient` wrappers | Keep as semantic command catalog. Stage 7B should expose equivalent TS typed operations. |
|
||||
| Render queries | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs:645`, `:974`, `:1079`, `:1165` | Keep as projection internals. Unity consumes typed zone/body/contact/viewport rows; asteroid instance poses are now carried by `zone_render` instead of recomputed from snapshots. |
|
||||
| Authority policy and health docs | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:45`; `AetheriaRuntimeVerseClient.GetHealthAsync` at `:237`; generated TS metadata in `Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts` | Keep. Stage 7 client startup must show policy id, peer health, daemon endpoint, frame id/time, and import counters. |
|
||||
|
||||
## Ad-Hoc Bus Search Results
|
||||
|
||||
Reviewed search terms:
|
||||
|
||||
```text
|
||||
command, viewport, Apply(command, payload, Queue, ConcurrentQueue, Channel,
|
||||
Bus, EventBus, CommandPort, cached command, port
|
||||
```
|
||||
|
||||
Actionable hits now:
|
||||
|
||||
- TypeScript raw CultMesh document put and snapshot request helpers.
|
||||
- Daemon RTS command/viewport request handlers.
|
||||
|
||||
Retired hits:
|
||||
|
||||
- Electron IPC `aetheria-rts:command` and `aetheria-rts:viewport`;
|
||||
- browser `window.aetheriaRts.command` and `window.aetheriaRts.viewport`;
|
||||
- public `RtsCommandRequest` command union;
|
||||
- `createCommandDocument`.
|
||||
|
||||
Non-actionable or already-quarantined hits:
|
||||
|
||||
- shader/material `Queue` metadata;
|
||||
- ordinary import/export/transport wording;
|
||||
- daemon peer endpoint option parsing;
|
||||
- command receipt/import bookkeeping from Stage 6.
|
||||
|
||||
No current `CommandPort`, `CachedCommandPort`, `EventBus`, or public
|
||||
`Apply(command, payload)` implementation was found in the scoped search.
|
||||
|
||||
## Stage 7D.1 Unity Edit Queue
|
||||
|
||||
Date: 2026-06-24
|
||||
|
||||
Stage 7D.0 verifier baseline passed after the Electron app-shell smoke was
|
||||
hardened. Unity parity may now begin, but only through the following queue.
|
||||
|
||||
Reviewed source search:
|
||||
|
||||
```powershell
|
||||
rg -n "ActionGameManager|AetheriaDaemonObserver|AetheriaRuntimeDaemonOperationClient|AetheriaRuntimeDaemonSurfaceCommands|ZoneRenderer|Rigidbody|Collider|Physics\.|SubmitDaemonCommand|CommandPort|CachedCommandPort|EventBus|Apply\(" Assets/Scripts Packages/org.gamecult.aetheria.state/Runtime -g "*.cs" -S
|
||||
rg -n "ObservedGalaxy|LoadZone|ApplyDaemonFrame|GameObject\.Find|FindObjects|GetComponent<" Assets/Scripts -g "*.cs" -S
|
||||
rg -n "Queue|Channel|ConcurrentQueue|Bus" Assets/Scripts Packages/org.gamecult.aetheria.state/Runtime -g "*.cs" -S
|
||||
```
|
||||
|
||||
### 7D.2 Facade Candidates
|
||||
|
||||
Status on 2026-06-24: initial shared facade is in place.
|
||||
`AetheriaClient` owns the local Verse client lifetime, exposes typed daemon
|
||||
operations, and provides named local reads for map viewports, selected object,
|
||||
inventory, daemon health, authority policy, and SoA view data.
|
||||
`AetheriaDaemonObserver` now exposes that shared client instead of keeping a
|
||||
separate operation client and Verse client. The remaining rows are either
|
||||
carried into 7D.3 command reroute or 7D.4 projection reroute.
|
||||
|
||||
| Surface | Evidence | Current Shape | Stage 7D Action |
|
||||
| --- | --- | --- | --- |
|
||||
| Unity local daemon observer | `Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs` | Now owns a shared `AetheriaClient`, local observation cursor, and SoA render mapping. | Keep as Unity presentation shell. 7D.3 should move remaining command ingress through `observer.Client`/`observer.Operations`; 7D.4 should move read sites to named facade projections. |
|
||||
| Runtime daemon operation client | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationClient.cs` | Typed command helper. The shared facade can now inject a submit delegate so sends reuse its long-lived Verse client instead of opening a new client per command. | Keep as typed command document builder behind `AetheriaClient`. Do not expose generic command APIs above it. |
|
||||
| Generated operation catalog | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationsClient.cs` | Ergonomic typed wrappers over `AetheriaRuntimeDaemonOperationClient`; nullable observed state is now explicit for pre-frame command issue. | Keep as public typed operation vocabulary exposed through `AetheriaClient.Operations`. |
|
||||
| Eve/CultUI surface commands | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSurfaceCommands.cs:8`, `:37`; catalog at `AetheriaRuntimeDaemonSurfaceCommandCatalog.cs:58` | Surface command ingress now routes through `AetheriaClient` and facade-backed daemon command document submission. | Keep as typed Eve ingress behind the facade. Do not let UI callers submit daemon documents directly. |
|
||||
| Runtime Verse client submit | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs:508`, `:528` | Canonical typed command document submission. | Keep as the low-level primitive used by the facade. Do not expose stringly command APIs above it. |
|
||||
|
||||
### 7D.3 Command Reroute Queue
|
||||
|
||||
| Surface | Evidence | Current Shape | Stage 7D Action |
|
||||
| --- | --- | --- | --- |
|
||||
| Input settings commands | `Assets/Scripts/UI/InputScreen/InputDisplayLayout.cs` | Panel now resolves and owns an `AetheriaClient`, then submits typed input settings values through `AetheriaClient.SubmitInputSettingsCommandAsync`. | Done for 7D.3. Use this as the pattern for remaining direct UI command reroutes. |
|
||||
| Loadout template save/delete | `Assets/Scripts/UI/Menu/InventoryPanel.cs`; projection helper at `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeLoadoutSnapshotProjector.cs` | `InventoryPanel` owns loadout-template save submission through `AetheriaClient.SubmitLoadoutTemplateCommandAsync` and now asks `AetheriaClient.LoadoutTemplateAsync(entityKey)` to project the target daemon entity into a template. Save no longer serializes the Unity `Entity` facade. | Done for save. The remaining Unity loadout projector is renderer/bootstrap adapter code for constructing Unity presentation objects from saved templates, not the save path. |
|
||||
| Loadout restore/current ship/name/hull toggles | `Assets/Scripts/UI/Menu/InventoryPanel.cs` | `InventoryPanel` owns an explicit `AetheriaClient` and submits `RestoreLoadout`, `SetDockedCurrentShip`, `SetEntityName`, and `ToggleHullConductivity` through `AetheriaClient.Operations`. The old manager request shims are gone; the panel uses the manager only to resolve observed Unity facade entities to daemon record keys and local price context. | Done for this dropdown/current-ship slice. Price/context logic that another runtime needs should move into daemon/state code before Stage 8. |
|
||||
| Inventory drag/drop and double-click item transfer | `Assets/Scripts/UI/Menu/InventoryPanel.cs`; `Assets/Scripts/UI/Menu/InventoryMenu.cs` | Inventory panels/menus own explicit `AetheriaClient` instances and submit `TransferCargoItem`, `EquipItem`, and `StoreItem` through typed facade operations. `ActionGameManager` no longer exposes cargo/equipment transfer request shims; it only resolves observed Unity facade cargo/equipment objects to daemon record keys and indices. | Done for this transfer slice. |
|
||||
| Trade purchases | `Assets/Scripts/UI/Menu/TradeMenu.cs` | `TradeMenu` owns an explicit `AetheriaClient` and submits typed `TradePurchase` operations for station stock, cargo purchases, commodity quantity purchases, and docked ship hull purchases. `ActionGameManager` no longer exposes trade purchase request shims. | Done for current trade purchase surface. Station-stock authoring/refit verbs still need first-class typed surfaces where they are not already represented by trade commands. |
|
||||
| Equipped-item details and action-bar binding commands | `Assets/Scripts/UI/Menu/InventoryMenu.cs`; `Assets/Scripts/Gameplay/ActionBarSlot.cs` | `InventoryMenu` submits equipped-item override shutdown, thermotoggle target temperature, weapon-group membership, action-bar bind, and action-bar clear through its explicit `AetheriaClient`. Restored action-bar bindings hold an explicit `AetheriaClient` and submit consumable, behavior-active, and weapon-group-active typed operations. `ActionGameManager` no longer exposes public request shims for these menu commands; it only resolves observed item identity and action-bar control paths. | Done for current detailed item/action-bar surface. Drag-to-action-bar still originates in the manager because drag registration currently lives there, but it submits through the shared facade rather than a daemon shim. |
|
||||
| Runtime target/control commands | `Assets/Scripts/Gameplay/ActionGameManager.cs` plus `AetheriaDaemonObserver.Operations` | `ActionGameManager` remains the local input gesture router for movement, look, tractor power, target selection/cycling, reticle targeting, override shutdown, sensor ping, heatsinks, shields, interact, tow, dock, and undock. It now submits those through a shared typed facade operation helper instead of private daemon shim methods. | Done for current pilot input ingress. Stage 8 should contract this to input orchestration only after equivalent projections replace manager-owned reads. |
|
||||
| Starbridge commander and pilot verbs | Starbridge design target in `E:/Projects/AetheriaLore/Aetheria/Game Design/Aetheria Starbridge.md` | Station stock, docking/refit, salvage, construction anchoring, target marks, cooling, repair, drone/turret orders, infrastructure placement, fabrication, wave/hostile control, and survival-pod recovery are not all first-class command surfaces yet. | Add them only as typed daemon/state operations or typed projections. Unity may issue pilot-local intent; RTS may issue commander intent; neither client may own bespoke gameplay behavior for these verbs. |
|
||||
| Menu startup and Eve commands | `Assets/Scripts/UI/MainMenu.cs:20`, `:461` | Main menu now owns an `AetheriaClient`; frame/settings reads and known Eve command submission route through the facade. Client-target file edits still use the typed local target utility. | Keep as facade-backed UI shell. Stage 8 should delete the menu-owned compatibility shims once panels can hold explicit client references. |
|
||||
| Eve surface presenter | `Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveSurfacePresenter.cs` | Presenter now owns an `AetheriaClient`; daemon Eve surface reads, state-ref resolution, and surface command submission use the facade or file-backed state reader. | Keep as Unity presentation infrastructure. It must remain render/input plumbing, not gameplay authority. |
|
||||
|
||||
### 7D.4 Projection And Read Queue
|
||||
|
||||
| Surface | Evidence | Current Shape | Stage 7D Action |
|
||||
| --- | --- | --- | --- |
|
||||
| Observed daemon state polling | `Assets/Scripts/Gameplay/AetheriaDaemonObserver.cs:74`, `:120` | Polls local Verse state and emits changed observations. | Keep as local read primitive, but expose named typed projections through 7D.2 facade rather than leaking raw observed state to gameplay shell callers. |
|
||||
| Observed galaxy facade | `Assets/Scripts/Gameplay/AetheriaUnityObservedRunProjection.cs`; booted from typed sector-map state in `Assets/Scripts/UI/MainMenu.cs` | Unity builds a projected Galaxy facade only as a legacy scene-construction adapter. `ActionGameManager` no longer stores or exposes it. | Quarantine for Stage 7D. Replace reads with local projection/state access where possible. Delete or collapse in Stage 8 after equivalent projections exist. |
|
||||
| Runtime catalog/settings reads | scoped search across `Assets/Scripts` and `Packages/org.gamecult.aetheria.state` | No direct `ActionGameManager.RuntimeCatalog` or `ActionGameManager.RuntimePlayerSettings` reads remain. `InventoryMenu`, `InventoryPanel`, `TradeMenu`, `ActionBarSlot`, `ZoneRenderer`, `EntityInstance`, `ShipInstance`, `InputDisplayLayout`, `VolumeCloudRenderer`, `MainMenu`, and `SchematicDisplay` have moved their local catalog/formatting/settings reads to explicit `AetheriaClient` instances or a renderer-owned client cache. | Done for direct manager-global catalog/settings cleanup. Continue replacing observed-galaxy/entity facade reads with portable typed projections. |
|
||||
| Map screen zone/settings reads | `Assets/Scripts/UI/Menu/MapRenderer.cs` | `MapRenderer` now owns an explicit `AetheriaClient` and reads zone title from `ObjectsViewportAsync` plus minimap asteroid visibility from `PlayerSettingsAsync`. The unused `ActionGameManager` reference was removed. | Done for this focused 7D.4 read slice. Continue moving map/sector reads off the temporary observed galaxy and global runtime settings where typed projections exist. |
|
||||
| Sector zone details reads | `Assets/Scripts/UI/Menu/SectorRenderer.cs` | The zone-details Eve surface now reads sector topology from `SectorMapAsync`, daemon zone facts from `ZoneDetailsAsync`, hull type lookups from `OpenRuntimeCatalog`, and formatting settings from `PlayerSettingsAsync` through an explicit `AetheriaClient`. It no longer asks `ActionGameManager` for zone snapshots, runtime catalog, or player formatting, and no longer reads the whole authoritative frame for this surface. | Done for zone-details surface. The sector map layout/reveal path still uses the legacy `GalaxyZone` facade until every remaining renderer signal is represented by portable typed projections. |
|
||||
| Target/contact presentation reads | `Assets/Scripts/Gameplay/AetheriaUnityObservedTargetQuery.cs`; `Assets/Scripts/Gameplay/AetheriaUnityTargetPresentation.cs`; projection in `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs` | Unity target presentation now reads `gamecult.aetheria.zone_contacts.v1` through `AetheriaClient.ZoneContactsAsync()`. The feed carries typed target/contact identity, visibility, hostility, target position, deltas, and distance for the current zone. Target HUD, target-distance renderer cache, minimap compass markers, and renderer visibility fades no longer call snapshot render-query helpers for those facts. | Done for current target HUD indicators, target-distance renderer cache, compass markers, and visibility fade state. Remaining target presentation objects are Unity affordances over typed contact rows. |
|
||||
| Docked current-entity binding | `Assets/Scripts/Gameplay/AetheriaUnityObservedDockingIndex.cs`; `Assets/Scripts/Gameplay/AetheriaUnityCurrentEntityBinder.cs`; `Assets/Scripts/Gameplay/AetheriaUnityCurrentEntityPresentation.cs` | Docking state now comes from `AetheriaClient.CurrentDockingAsync()` and resolves the Unity docking-bay facade by typed dock parent key plus bay index. Dock camera body focus uses `AetheriaClient.ZoneRenderAsync()` body poses rather than passing a raw current-zone snapshot into presentation. | Done for current docked binding path. The remaining Unity facade entity/docking-bay objects are presentation adapters, not portable gameplay state. |
|
||||
| Inventory/trade item detail reads | `Assets/Scripts/UI/Menu/InventoryMenu.cs`; `Assets/Scripts/UI/Menu/InventoryPanel.cs`; `Assets/Scripts/UI/Menu/TradeMenu.cs` | These panels already own explicit `AetheriaClient` instances for typed command submission. They now also read runtime catalog/manufacturer data and player formatting settings through those local clients for ship settings, cargo item details, equipped item details, trade table columns, trade behavior fields, trade item details, dropdown loadout pricing, inventory hull/item geometry, hardpoint typing, durability lookup, and temperature labels. | Done for current detail/dropdown/panel slice. Remaining HUD schematic, entity prefab presentation, and action-bar icon/name reads still use manager globals until migrated. |
|
||||
| Current ship settings facts | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs`; `Assets/Scripts/UI/Menu/InventoryMenu.cs` | `CurrentEntityAsync` now publishes the current entity shutdown-performance threshold. `InventoryMenu` renders the ship-settings Eve surface from the typed current-entity document and submits shutdown changes by daemon entity key instead of storing a Unity `Entity` facade as the settings source. | Done for current ship-settings read slice. The daemon still validates range and applies authority; Unity only renders the typed row and submits intent. |
|
||||
| Current cockpit HUD facts | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs`; `Assets/Scripts/UI/HUD/SchematicDisplay.cs` | `CurrentEntityAsync` now publishes a typed current-entity HUD status row for override shutdown, shield activity, heatsinks, heat/hypothermia exposure, visibility, hull ratio, radiator range, sensor cooldown, reactor draw, capacitor charge, and Aether-drive RPM. The player schematic HUD reads that row through its local `AetheriaClient` instead of pulling those facts from the Unity `Entity` facade. | Done for the current player HUD status slice. Target/enemy schematic rows still adapt Unity presentation objects until a target-entity projection replaces that display path. |
|
||||
| Station refit docking-bay rows | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs`; `Assets/Scripts/UI/Menu/InventoryPanel.cs`; `Assets/Scripts/UI/Menu/InventoryMenu.cs` | `StationRefitAsync` now publishes typed docking-bay rows with slot identity, occupied entity identity, current-entity match, hull key, and cargo items. Inventory menu/panel docking-bay display validates the current row before adapting the Unity docking-bay facade. | Done for the current docking-bay display slice. Continue promoting station stock/refit eligibility and remaining cargo presentation facts into typed station/refit projections before Stage 8. |
|
||||
| Loadout restore options | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaClient.cs`; `Assets/Scripts/UI/Menu/InventoryPanel.cs` | `StationRefitAsync` now composes daemon frame state, Verse loadout templates, and runtime catalog policy into typed restore options. Each row carries template name, daemon target entity key, price, and `CanRestore`. InventoryPanel renders and submits those rows instead of reading `LoadoutTemplatesAsync` or pricing templates locally. | Done for current restore dropdown slice. The daemon still validates restore acceptance on command apply; clients only display the projected row and submit intent. |
|
||||
| Trade cargo target rows | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs`; `Assets/Scripts/UI/Menu/TradeMenu.cs` | `StationRefitAsync` now publishes typed cargo target rows for the current docking bay and player ship bays. Each row carries target kind, label, entity key, bay index, hull key, and cargo items. TradeMenu builds the target selector and owned counts from those rows instead of reconstructing targets from available entities or treating station stock as docking-bay cargo. | Done for current target cargo/count slice. Remaining trade row filtering and purchase submission stay typed; Stage 8 can delete more Unity presentation adapters once row rendering is fully projection-native. |
|
||||
| Station stock trade facts | `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsProjection.cs`; `Assets/Scripts/UI/Menu/TradeMenu.cs` | `StationRefitAsync` enriches each station stock row with shared-policy price, affordability, and owned quantity. Hull ownership is counted from typed available entities; item ownership is counted from typed cargo target rows. TradeMenu renders the `OwnedQuantity` row fact instead of counting Unity ships or cargo dictionaries locally. | Done for current stock row fact slice. The daemon projection owns trade row display facts; command apply still revalidates purchase acceptance. |
|
||||
| Action-bar and renderer catalog reads | `Assets/Scripts/Gameplay/ActionBarSlot.cs`; `Assets/Scripts/Zone Display/ZoneRenderer.cs`; `Assets/Scripts/Gameplay/EntityInstance.cs`; `Assets/Scripts/Gameplay/ShipInstance.cs` | Action-bar bindings resolve gear icon catalog data through their explicit `AetheriaClient`. `ZoneRenderer` owns a local `AetheriaClient` catalog cache used for entity hull prefabs, pickup labels, pickup tier color, entity hull binding, and ship durability normalization. | Done for current presentation catalog slice. Remaining HUD schematic catalog/formatting and main-menu observed-galaxy boot still need separate treatment. |
|
||||
| Input and nebula settings reads | `Assets/Scripts/UI/InputScreen/InputDisplayLayout.cs`; `Assets/Scripts/Zone Display/VolumeCloudRenderer.cs` | The input screen reads action-bar input visibility from `AetheriaClient.PlayerSettingsAsync()`. The volume cloud renderer reads nebula quality from `AetheriaClient.PlayerSettingsAsync()` and falls back to its serialized quality if local state cannot be read. | Done for current settings slice. |
|
||||
| Main-menu and HUD catalog/settings reads | `Assets/Scripts/UI/MainMenu.cs`; `Assets/Scripts/UI/HUD/SchematicDisplay.cs` | Main menu reads the runtime catalog and typed sector-map boot state through its local `AetheriaClient`; the legacy observed-galaxy boot projection now lowers from `SectorMapAsync` rather than a whole authoritative daemon frame. Schematic HUD reads item catalog data and player formatting settings through its local `AetheriaClient`. | Done for direct manager-global read cleanup and whole-frame boot read removal. `SchematicDisplay.cs` was transcoded to UTF-8 before editing because it contained legacy non-UTF8 degree-symbol bytes. |
|
||||
| Trade value settings projection | `Aetheria.State/Documents/AetheriaTradeValuePolicy.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs`; `Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs`; `InventoryMenu.cs`; `InventoryPanel.cs`; `TradeMenu.cs`; `ZoneRenderer.cs` | Trade value projection is now authored typed state. The daemon seeds `global:aetheria.trade_value_policy.v1`, provider advertisements include `aetheria.trade_value_policy.v1`, and runtime catalog snapshots read that policy into `AetheriaRuntimeCatalogSnapshot.TradeValueSettings`. Unity panels/renderers consume it through explicit `AetheriaClient` catalog caches. The Unity `GameSettings` projection helper and `ActionGameManager.ObservedTradeValueSettings` are gone. | Done for manager, Unity-settings source, and code-only default cleanup. Next policy work is designer-facing Eve/CultUI authoring, not another Unity settings bridge. |
|
||||
| Sector map reads | `Assets/Scripts/UI/Menu/SectorMap.cs`; `Assets/Scripts/UI/Menu/SectorRenderer.cs`; `Assets/Scripts/UI/MainMenu.cs` | Sector map renders from `AetheriaClient.SectorMapAsync`, emits clicked zone indices, and resolves details from `SectorMapAsync` plus daemon zone contents. Main-menu observed-game boot also uses `SectorMapAsync` for run id, frame id, tutorial mode, generation seed, zones, links, discovery, and faction relationships. It no longer reads `ActionGameManager.TryGetObservedGalaxy`, passes `GalaxyZone` objects through UI events, or reads whole daemon frames for Unity observed-galaxy boot. | Done for the current topology/detail/boot surface. Remaining work is richer faction/home/boss icon metadata as typed sector-map fields if designers still need those signals. |
|
||||
| Frame lowering | `Assets/Scripts/Gameplay/AetheriaUnityObservedFrameApplier.cs`; `Assets/Scripts/Gameplay/AetheriaUnityObservedRunProjection.cs`; `Assets/Scripts/Gameplay/AetheriaUnityObservedZoneContextProjector.cs`; `Assets/Scripts/Gameplay/ActionGameManager.cs` | Frame handoff is now named as `ApplyLatestZoneRender`: it reads `ZoneRenderAsync()` and adapts only the target zone needed for the Unity presentation shell. The frame applier no longer receives the whole observed galaxy facade, the temporary projected-zone lookup lives beside the observed-run projection holder, and `ActionGameManager` no longer keeps an observed-galaxy property. The remaining whole-graph dependency is lazily resolved inside the quarantined `Zone` construction adapter because the legacy `Zone` constructor still requires it. | Keep narrowing toward typed projection/native render consumption. Persistent gameplay mirror belongs to Stage 8 demolition. |
|
||||
|
||||
### 7D.5 Renderer/Input Shell Queue
|
||||
|
||||
| Surface | Evidence | Current Shape | Stage 7D Action |
|
||||
| --- | --- | --- | --- |
|
||||
| Zone renderer frame application | `Assets/Scripts/Zone Display/ZoneRenderer.cs:220`, `:239` | `LoadDaemonZoneView` clears/rebuilds a Unity hierarchy; `ApplyZoneRender` updates render-time zone data, caches target/contact rows from `ZoneContactsAsync()`, loads presentation entities from `ObjectsViewportAsync(...)`, and discovers body views plus terrain sampling facts from `GravityViewportAsync(...)` for the current XY bounds. Unity no longer recomputes target, compass, visibility, presentation entity, visible body-view, terrain-height, terrain-band, asteroid-instance, dropped-pickup, entity-facade, orbit, or body facts from raw zone snapshot render queries. | Keep only as render cache. `ApplyZoneRender` is acceptable as per-frame projection consumption; `LoadDaemonZoneView` is now the remaining Stage 8 hierarchy rebuild shim once renderer data is native/projection-driven. |
|
||||
| Entity instance cache | `ZoneRenderer.cs`; `AetheriaUnityObservedFacadeIndex.cs`; target presentation wiring in `AetheriaUnityGameplaySceneWiring.cs` | Unity keeps daemon-indexed presentation objects for rendering, docking, target HUD, and camera binding. Daemon-index entity lookup lives in the explicit facade index/scene wiring instead of an `ActionGameManager` shim. | Classify as temporary renderer shell. It must not become gameplay authority; replace with render/native views as Stage 8 proceeds. |
|
||||
| SoA render fast path | `Assets/Scripts/Gameplay/AetheriaDaemonRenderBuffer.cs:50`, `AetheriaDaemonIndirectRenderer.cs:40`, `AetheriaDaemonObserver.LastRenderNativeView` | Reads observer SoA/native view for rendering. | Keep. This is aligned with thin renderer direction. Attach it to the 7D.2 facade instead of direct observer singleton lookup. |
|
||||
| Ymir click/physics bridge | `Assets/Scripts/Gameplay/Physics/AetheriaYmirPhysicsBridge.cs` | Uses Ymir/clickable/hull mappings, not `UnityEngine.Physics` as simulation authority. | Keep as presentation/query bridge, then narrow behind facade/render shell. Any remaining Unity collider components are click/render affordances, not gameplay physics authority. |
|
||||
| `GetComponent` and prefab collider hits | many source/prefab hits | Mostly Unity presentation wiring, object pooling, click affordances, and imported asset metadata. | Non-actionable for 7D unless the component participates in gameplay authority. Recheck during Stage 8 demolition. |
|
||||
|
||||
### Non-Actionable Queue Hits
|
||||
|
||||
| Hit | Evidence | Decision |
|
||||
| --- | --- | --- |
|
||||
| Event log queue | `Assets/Scripts/EventLog.cs:13` | UI log buffer; not a command bus. Keep. |
|
||||
| Sector reveal queue | `Assets/Scripts/UI/Menu/SectorMap.cs:54`, `:79` | Presentation animation queue; not a gameplay command bus. Keep until the sector map projection is replaced. |
|
||||
| KD-tree query queue | `Assets/Scripts/ServerShared/NIH/KDTree/KDQuery/*.cs` | Internal spatial query implementation. Keep unless replaced by CultMath/Ymir spatial structures in a later performance pass. |
|
||||
| `System.Threading.Channels.dll` asmdef reference | `Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef` | Assembly reference only; no ad hoc runtime channel found in scoped source. |
|
||||
| `Physics.` source search | scoped C# search | No actionable `UnityEngine.Physics.*` simulation authority call found in `Assets/Scripts` or state runtime during this pass. |
|
||||
|
||||
## Stage 7B Build Order
|
||||
|
||||
Done:
|
||||
|
||||
- public renderer API no longer exposes `window.aetheriaRts.command` or
|
||||
`window.aetheriaRts.viewport`;
|
||||
- Electron IPC no longer exposes `aetheria-rts:command` or
|
||||
`aetheria-rts:viewport`;
|
||||
- browser code now calls typed methods `setMoveVector`, `setTarget`, and
|
||||
`mapViewport`;
|
||||
- `aetheria-cultmesh.ts` is now transport-focused and no longer owns
|
||||
MessagePack slot layout;
|
||||
- local generated-style bindings in `aetheria-rts-bindings.ts` encode/decode
|
||||
named slots for command documents, including the 30-slot daemon command
|
||||
layout;
|
||||
- `scripts/generate-rts-bindings.mjs` derives schema ids, command kind ids, and
|
||||
slot maps from the C# MessagePack document declarations into
|
||||
`aetheria-rts-generated-bindings.ts` for daemon command, daemon frame, daemon
|
||||
health, RTS viewport payloads, nested daemon snapshot payloads, Verse
|
||||
authority policy, authority rule, and authority lease;
|
||||
- `AetheriaRuntimeRtsProjection` owns typed viewport projection over local
|
||||
daemon frame state, including controlled-unit visibility union, status,
|
||||
inventory, and gravity influence intersections;
|
||||
- daemon RUDP snapshot handling delegates RTS viewport construction to
|
||||
`AetheriaRuntimeRtsProjection` instead of carrying map projection logic
|
||||
inline;
|
||||
- `Aetheria.State.AuthoritySmoke` verifies the projection behavior against a
|
||||
local daemon frame;
|
||||
- Electron map reads now fetch the latest daemon frame and project it locally in
|
||||
`aetheria-rts-local-projection.ts` instead of requesting the remote RTS
|
||||
viewport document;
|
||||
- Electron selected-object, inventory/cargo, daemon health, and authority status
|
||||
panels now read through typed local projection facade methods instead of
|
||||
deriving panel state sideways from the map payload;
|
||||
- Electron map/panel projection reads now use local CultCache publication files
|
||||
through `aetheria-local-publication-reader.ts` instead of loopback
|
||||
`sendSnapshotRequest`;
|
||||
- the daemon publishes the Verse authority policy to a local `.authority.policy.cc`
|
||||
sidecar so Electron can read authority status locally alongside frame and
|
||||
health;
|
||||
- `Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1` starts a one-shot
|
||||
daemon and proves the compiled Electron runtime facade can read map,
|
||||
selected-object, inventory, health, and authority projections from local
|
||||
CultCache publications;
|
||||
- `Aetheria.Rts.Web/scripts/verify-stage7c-electron-shell.ps1` launches the real
|
||||
Electron app shell against a disposable runtime directory and proves the
|
||||
renderer can refresh through preload IPC using the typed projection facade;
|
||||
- Electron preload/main expose typed projection channels:
|
||||
`aetheria-rts:selected-object`, `aetheria-rts:inventory`,
|
||||
`aetheria-rts:daemon-health`, and `aetheria-rts:authority-status`;
|
||||
- `Aetheria.Rts.Web/scripts/verify-stage7b-rts-client.ps1` fails if the old
|
||||
public generic API returns or if the transport wrapper regains document
|
||||
layout/codec ownership, fails if stale remote viewport decoder helpers return,
|
||||
and also fails if the generated bindings are stale.
|
||||
|
||||
Remaining:
|
||||
|
||||
1. Move Unity command/read paths onto the same typed local client shape proven by
|
||||
Electron.
|
||||
2. Add a separate peer sync health projection if daemon health stops being the
|
||||
canonical peer health surface.
|
||||
3. Replace the remaining semantic command construction arrays with a CultMesh
|
||||
TS typed document writer once that primitive is available locally.
|
||||
4. Narrow Electron preload/main IPC behind a typed runtime facade.
|
||||
5. Keep the daemon RTS viewport handler only as temporary compatibility plumbing
|
||||
until no client or diagnostic path depends on it.
|
||||
6. Keep the verifier checking for old public generic surfaces and transport
|
||||
wrapper layout leaks:
|
||||
- `window.aetheriaRts.command`;
|
||||
- `window.aetheriaRts.viewport`;
|
||||
- `ipcMain.handle("aetheria-rts:command"`;
|
||||
- `ipcMain.handle("aetheria-rts:viewport"`;
|
||||
- `function createCommandDocument`;
|
||||
- `decodeViewportDocument`;
|
||||
- `viewportRecordKey`;
|
||||
- public TS command union `{ kind: ... }` used as transport payload;
|
||||
- hand-indexed MessagePack document layout in `aetheria-cultmesh.ts`.
|
||||
|
||||
## Stage 7A Exit Check
|
||||
|
||||
This inventory is the Stage 7A artifact. Stage 7A has exited. Electron's Stage
|
||||
7B/7C proving surface now has generated binding metadata, typed command facade
|
||||
methods, local map/panel projections, local CultCache publication reads, and
|
||||
runtime/app-shell verifiers. The next use of this file is Stage 7D.1: refresh
|
||||
the Unity entries into a concrete edit queue before changing Unity behavior.
|
||||
File diff suppressed because it is too large
Load diff
1877
Aetheria.State/docs/verse-authority-implementation-plan.md
Normal file
1877
Aetheria.State/docs/verse-authority-implementation-plan.md
Normal file
File diff suppressed because it is too large
Load diff
226
Aetheria.State/docs/verse-authority-policy.md
Normal file
226
Aetheria.State/docs/verse-authority-policy.md
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
# Verse Authority Policy
|
||||
|
||||
Aetheria authority is Verse policy, not topology. A Raven Unity client, a
|
||||
Starfire RTS client, a headless host, and later an MMO witness quorum should all
|
||||
use the same policy boundary:
|
||||
|
||||
```text
|
||||
typed operation
|
||||
-> resolve subject and claim kind
|
||||
-> resolve authority mode from the active Verse policy
|
||||
-> apply, forward, defer, or reject
|
||||
```
|
||||
|
||||
The first implementation is intentionally fast for trusted co-op. It does not
|
||||
build quorum consensus on the hot path. It compiles the active policy document
|
||||
and authority leases into an in-memory routing table, then tests incoming typed
|
||||
commands before simulation.
|
||||
|
||||
## Policy Modes
|
||||
|
||||
The policy schema names the authority shapes Aetheria needs without requiring
|
||||
all of them to be implemented at once.
|
||||
|
||||
- `any-trusted-runtime`: trusted co-op mode. Any runtime with the active Verse
|
||||
rules may author matching claims.
|
||||
- `host-authoritative`: only the configured host runtime may author matching
|
||||
claims.
|
||||
- `delegated-runtime`: one of the configured runtime ids may author matching
|
||||
claims.
|
||||
- `owning-runtime`: the runtime that owns the subject may author matching
|
||||
claims. This is schema-level intent for now; ownership resolution must be
|
||||
supplied by the runtime before this mode is enabled.
|
||||
- `interest-lease`: a runtime with a matching unexpired lease may author
|
||||
matching claims.
|
||||
- `witness-quorum`: eligible witnesses publish observations; policy forms a
|
||||
candidate; authority commits a fact. This is not implemented in the current
|
||||
Aetheria hot path.
|
||||
- `operator-finality`: operator or host finality is required before the fact is
|
||||
committed.
|
||||
- `mergeable-crdt`: independently authored state converges through explicit
|
||||
merge laws. This is for state with real merge semantics, not simulation facts.
|
||||
|
||||
Unsupported modes fail closed with an explicit policy rejection. This keeps the
|
||||
schema open without pretending future authority structures already exist.
|
||||
|
||||
## Command Claims
|
||||
|
||||
Daemon commands are lowered to a subject and claim kind before execution:
|
||||
|
||||
```text
|
||||
subject key: entity/run/zone/item key affected by the operation
|
||||
claim kind: movement, targeting, combat, inventory, economy, ai, metadata...
|
||||
author runtime: command.AuthorRuntimeId, falling back to ClientId
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```text
|
||||
SetMoveVector(entity.player.raven)
|
||||
-> subject entity.player.raven
|
||||
-> claim movement
|
||||
|
||||
SetTarget(entity.player.raven -> entity.hostile.3)
|
||||
-> subject entity.player.raven
|
||||
-> claim targeting
|
||||
|
||||
TradePurchase(station -> player cargo)
|
||||
-> subject target/destination entity
|
||||
-> claim economy
|
||||
```
|
||||
|
||||
The command executor only sees authorized commands. It should remain boring:
|
||||
validate payload shape, mutate the run, emit intents. Authority decisions belong
|
||||
to the policy router.
|
||||
|
||||
## Trusted Co-Op Shape
|
||||
|
||||
The default Aetheria policy is permissive trusted co-op:
|
||||
|
||||
```text
|
||||
*
|
||||
*
|
||||
any-trusted-runtime
|
||||
```
|
||||
|
||||
Raven/Starfire tests can then tighten this without changing gameplay systems:
|
||||
|
||||
```text
|
||||
entity.player.raven
|
||||
movement,targeting,combat
|
||||
delegated-runtime: raven-unity
|
||||
|
||||
entity.rts.starfire.
|
||||
movement,targeting,ai
|
||||
delegated-runtime: starfire-rts
|
||||
|
||||
entity.hostile.
|
||||
ai,movement,targeting
|
||||
delegated-runtime: starfire-rts
|
||||
|
||||
entity.hostile.
|
||||
close-combat-response,combat
|
||||
interest-lease
|
||||
```
|
||||
|
||||
That leaves the door open to witness-authoritative MMO policy later while
|
||||
keeping the co-op hot path as a few string/prefix checks and set lookups.
|
||||
|
||||
## Simulation Host Deployment Shapes
|
||||
|
||||
Aetheria should not require the C# daemon process to exist as a separately
|
||||
deployed executable. A Verse needs at least one runtime capable of advancing
|
||||
simulation, but that runtime can be packaged in different surfaces:
|
||||
|
||||
- `dedicated-daemon`: a standalone C# daemon owns simulation and publishes
|
||||
daemon frames.
|
||||
- `electron-launched-daemon`: Electron deploys/launches the same daemon and the
|
||||
browser surface talks to the local CultMesh node.
|
||||
- `unity-host`: one Unity player runs the Aetheria simulation host in-process
|
||||
or as an embedded sidecar and advertises itself as the host runtime for the
|
||||
Verse.
|
||||
- `browser-wasm-host`: a browser runtime runs a WebAssembly simulation kernel
|
||||
and advertises itself as the simulation host for a Verse.
|
||||
- `native-kernel-host`: a native Rust/CultLib runtime runs the same simulation
|
||||
kernel outside Unity and outside the current C# daemon executable.
|
||||
- `distributed-trusted`: trusted Unity/RTS runtimes author claim subsets
|
||||
directly according to policy; each still submits typed facts through the same
|
||||
authority gate.
|
||||
- `observer-only`: browser/Eve/RTS clients render and submit typed commands but
|
||||
do not advance simulation.
|
||||
|
||||
The important boundary is not the executable. It is the policy document:
|
||||
|
||||
```text
|
||||
browser CultMesh client
|
||||
-> typed command/proposal document
|
||||
-> Verse authority policy
|
||||
-> simulation-capable runtime lease/host
|
||||
-> committed facts and local projections
|
||||
```
|
||||
|
||||
If no dedicated daemon is deployed, a Unity player may become the simulation
|
||||
host by advertising a stable runtime id and by using `host-authoritative` or
|
||||
delegated rules that name that Unity runtime. Browser-only clients remain thin:
|
||||
they can control and observe the Verse through CultMesh, but they do not become
|
||||
authoritative unless a TS/WASM simulation runtime is explicitly added and named
|
||||
by policy.
|
||||
|
||||
The first co-op fallback should be:
|
||||
|
||||
```text
|
||||
simulation host: raven-unity
|
||||
default mode: host-authoritative
|
||||
|
||||
*
|
||||
*
|
||||
host-authoritative
|
||||
```
|
||||
|
||||
Then Starbridge can refine hot claims without changing client protocols:
|
||||
|
||||
```text
|
||||
entity.player.raven
|
||||
movement,targeting,combat
|
||||
delegated-runtime: raven-unity
|
||||
|
||||
entity.hostile.
|
||||
ai,movement,targeting
|
||||
delegated-runtime: starfire-rts
|
||||
|
||||
*
|
||||
inventory,economy,system
|
||||
host-authoritative: raven-unity
|
||||
```
|
||||
|
||||
This keeps the browser client honest: it can run CultMesh and submit typed
|
||||
commands, but someone with the Aetheria simulation assembly must own the
|
||||
simulation work.
|
||||
|
||||
## Current Local-Mirror Constraint
|
||||
|
||||
The current C# implementation is not a pure remote-client architecture yet.
|
||||
Unity can select a remote CultMesh Verse, but gameplay boot still requires that
|
||||
Verse to be hydrated into a readable local CultCache replica before the Unity
|
||||
shell starts. The Electron RTS client likewise launches a local daemon and reads
|
||||
local daemon publications while using CultMesh for typed command submission.
|
||||
|
||||
That constraint is acceptable as a migration bridge, but it must not become the
|
||||
architecture. The end state is:
|
||||
|
||||
```text
|
||||
CultMesh transport/subscription
|
||||
-> typed Verse documents and operations
|
||||
-> local runtime projection/cache when useful
|
||||
-> renderer/input surface
|
||||
```
|
||||
|
||||
The local cache is an optimization and offline/debugging aid. It is not the
|
||||
authority boundary.
|
||||
|
||||
## Rust/WASM Simulation Kernel Migration
|
||||
|
||||
If browser-only clients are expected to take over simulation authority when no
|
||||
local .NET daemon is available, the daemon should be split into an embeddable
|
||||
simulation kernel and thin host shells. Rust is the strongest candidate for that
|
||||
kernel because the same core can target native hosts and WebAssembly without
|
||||
preserving the current local process/file assumptions.
|
||||
|
||||
The staged migration is:
|
||||
|
||||
1. Freeze the simulation contract in typed CultMesh/CultCache documents:
|
||||
command inputs, authority policy inputs, tick inputs, committed facts,
|
||||
frame/projection outputs, and deterministic error outputs.
|
||||
2. Keep the C# daemon as the compatibility host while extracting every hot
|
||||
simulation decision behind that contract.
|
||||
3. Port the deterministic simulation kernel to Rust with matching schema
|
||||
bindings and fixture-based parity tests against the C# host.
|
||||
4. Compile the Rust kernel to native and WASM. Native hosts cover dedicated
|
||||
daemon, Electron-launched daemon, and Unity sidecar deployment. WASM covers
|
||||
browser simulation host deployment.
|
||||
5. Replace local-file-only readers with CultMesh subscriptions plus optional
|
||||
local cache hydration, so remote Verse access does not require a C# process
|
||||
on the same machine.
|
||||
|
||||
The C# daemon should then become one host shell among several, not the canonical
|
||||
definition of simulation authority.
|
||||
|
|
@ -12,7 +12,7 @@ Witness-authoritative networking sharpens this rule. Unity should not be a dumb
|
|||
|
||||
Unity-side entity graph creation must name the boundary it is crossing. `EntityConstructionBlueprintProjector.InstantiateAuthoritativeFromBlueprint` is for local authoritative construction paths that still need to be migrated into the daemon; daemon frames use `EntityConstructionBlueprintProjector.ProjectObservedFromBlueprint` to lower observed state into Unity objects. A Unity frame consumer must not call the authoritative construction entry point.
|
||||
|
||||
The same rule applies at the galaxy shell. `Galaxy.ProjectObservedDaemonRun` may build a Unity navigation/rendering projection from an authoritative daemon run frame. Main menu boot must not call a `Galaxy` constructor directly or run procedural generation; the daemon owns the run and Unity lowers it. The Unity-side public handle is `ActionGameManager.ObservedGalaxy`, not `CurrentGalaxy`, because it is a projection of daemon state.
|
||||
The same rule applies at the galaxy shell. `Galaxy.ProjectObservedSectorMap` may build a temporary Unity navigation/rendering projection from the typed sector-map document. Main menu boot must not call a `Galaxy` constructor directly, run procedural generation, or read whole daemon frames for this path; the daemon owns the run and Unity lowers typed projection state. The Unity-side handle is `AetheriaUnityObservedRunProjection`, not an `ActionGameManager` gameplay property, because the graph is a quarantined scene adapter rather than portable client state.
|
||||
|
||||
VoidBot's Odin MCP shape is the useful boundary model for agents: list providers through Odin, list Verses through Odin, load a provider-owned interface context through Odin, and invoke only commands that the provider interface advertises. The MCP is not a Brokkr special case and not a raw daemon socket wrapper.
|
||||
|
||||
|
|
|
|||
661
Aetheria.State/scripts/verify-stage7d-unity-parity.ps1
Normal file
661
Aetheria.State/scripts/verify-stage7d-unity-parity.ps1
Normal file
|
|
@ -0,0 +1,661 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
||||
Set-Location $repoRoot
|
||||
|
||||
function Read-Text([string] $path) {
|
||||
return [System.IO.File]::ReadAllText((Join-Path $repoRoot $path))
|
||||
}
|
||||
|
||||
function Assert-Contains([string] $text, [string] $needle, [string] $message) {
|
||||
if (-not $text.Contains($needle)) {
|
||||
throw $message
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-NotContains([string] $text, [string] $needle, [string] $message) {
|
||||
if ($text.Contains($needle)) {
|
||||
throw $message
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-NotMatches([string] $text, [string] $pattern, [string] $message) {
|
||||
if ([regex]::IsMatch($text, $pattern)) {
|
||||
throw $message
|
||||
}
|
||||
}
|
||||
|
||||
$mainMenu = Read-Text "Assets\Scripts\UI\MainMenu.cs"
|
||||
Assert-Contains $mainMenu "SetRuntimeInputScreenShell(Func<bool> canOpenRuntimeInputScreen, Action openRuntimeInputScreen)" "MainMenu must receive runtime input-screen handoff explicitly."
|
||||
Assert-Contains $mainMenu "_canOpenRuntimeInputScreen?.Invoke() == true" "MainMenu must query runtime input-screen availability through injected shell."
|
||||
Assert-Contains $mainMenu "_openRuntimeInputScreen?.Invoke();" "MainMenu must open runtime input screen through injected shell."
|
||||
Assert-Contains $mainMenu "LatestSectorMap" "MainMenu observed-game boot must read typed sector-map state."
|
||||
Assert-Contains $mainMenu ".SectorMapAsync()" "MainMenu observed-game boot must use the typed sector-map facade."
|
||||
Assert-NotContains $mainMenu "LatestDaemonFrame" "MainMenu must not read whole daemon frames for observed-game boot."
|
||||
Assert-NotContains $mainMenu "LatestAuthoritativeRunFrameAsync" "MainMenu must not read whole daemon frames for observed-game boot."
|
||||
Assert-NotContains $mainMenu "ActionGameManager.Instance.ShowInputScreenFromMenu" "MainMenu must not open runtime input screen through ActionGameManager.Instance."
|
||||
Assert-NotContains $mainMenu "ActionGameManager.Instance.CanShowInputScreenFromMenu" "MainMenu must not query runtime input screen through ActionGameManager.Instance."
|
||||
|
||||
$mapRenderer = Read-Text "Assets\Scripts\UI\Menu\MapRenderer.cs"
|
||||
Assert-Contains $mapRenderer "ObjectsViewportAsync" "MapRenderer must exercise the split objects viewport facade."
|
||||
Assert-Contains $mapRenderer "GravityViewportAsync" "MapRenderer must exercise the split gravity viewport facade."
|
||||
Assert-Contains $mapRenderer "PlayerSettingsAsync" "MapRenderer must read player settings through AetheriaClient."
|
||||
Assert-NotContains $mapRenderer "public ActionGameManager" "MapRenderer must not regain an ActionGameManager serialized dependency."
|
||||
Assert-NotContains $mapRenderer "LatestAuthoritativeRunFrameAsync" "MapRenderer must not regress to whole-frame reads for map viewport data."
|
||||
Assert-NotContains $mapRenderer "ActionGameManager.RuntimePlayerSettings" "MapRenderer must not read manager-global player settings."
|
||||
|
||||
$sectorRenderer = Read-Text "Assets\Scripts\UI\Menu\SectorRenderer.cs"
|
||||
Assert-Contains $sectorRenderer "AetheriaClient" "SectorRenderer must keep explicit local Verse client ownership."
|
||||
Assert-Contains $sectorRenderer "CurrentZoneAsync" "SectorRenderer must center from the typed current-zone facade."
|
||||
Assert-Contains $sectorRenderer "SectorMapAsync" "SectorRenderer zone details must read sector topology through AetheriaClient."
|
||||
Assert-Contains $sectorRenderer "ZoneDetailsAsync" "SectorRenderer zone details must read daemon zone facts through AetheriaClient."
|
||||
Assert-Contains $sectorRenderer "OpenRuntimeCatalog" "SectorRenderer zone details must read catalog data through AetheriaClient."
|
||||
Assert-Contains $sectorRenderer "PlayerSettingsAsync" "SectorRenderer zone details must read formatting settings through AetheriaClient."
|
||||
Assert-NotContains $sectorRenderer "public ActionGameManager" "SectorRenderer must not regain an ActionGameManager serialized dependency."
|
||||
Assert-NotContains $sectorRenderer "TryGetObservedRunZone" "SectorRenderer must not ask ActionGameManager for the current zone."
|
||||
Assert-NotContains $sectorRenderer "ActionGameManager.RuntimeCatalog" "SectorRenderer must not read manager-global runtime catalog."
|
||||
Assert-NotContains $sectorRenderer "ActionGameManager.RuntimePlayerSettings" "SectorRenderer must not read manager-global player settings."
|
||||
Assert-NotContains $sectorRenderer "TryGetObservedZoneSnapshot" "SectorRenderer zone details must not ask ActionGameManager for daemon zone snapshots."
|
||||
Assert-NotContains $sectorRenderer "LatestAuthoritativeRunFrameAsync" "SectorRenderer zone details must not read whole daemon frames."
|
||||
|
||||
$sectorMap = Read-Text "Assets\Scripts\UI\Menu\SectorMap.cs"
|
||||
Assert-Contains $sectorMap "SectorMapAsync" "SectorMap must render from the typed sector-map projection."
|
||||
Assert-Contains $sectorMap "Subject<int>" "SectorMap clicks must expose typed zone indices, not Unity GalaxyZone objects."
|
||||
Assert-NotContains $sectorMap "TryGetObservedGalaxy" "SectorMap must not ask ActionGameManager for observed galaxy state."
|
||||
Assert-NotContains $sectorMap "GalaxyZone" "SectorMap must not retain the legacy GalaxyZone graph."
|
||||
Assert-NotContains $sectorMap "Dictionary<Faction" "SectorMap must not retain legacy Faction graph materials."
|
||||
|
||||
$inventoryPanel = Read-Text "Assets\Scripts\UI\Menu\InventoryPanel.cs"
|
||||
Assert-Contains $inventoryPanel "CurrentEntityAsync" "InventoryPanel current-subject UI must read the typed current-entity facade."
|
||||
Assert-Contains $inventoryPanel "CurrentDockingAsync" "InventoryPanel dropdown docking access must read the typed current-docking facade."
|
||||
Assert-Contains $inventoryPanel "StationRefitAsync" "InventoryPanel dropdown entity options must read the typed station-refit facade."
|
||||
Assert-Contains $inventoryPanel "LoadoutRestoreOptions" "InventoryPanel loadout dropdown must read typed station-refit restore options."
|
||||
Assert-Contains $inventoryPanel "LoadoutTemplateAsync(targetEntityKey)" "InventoryPanel loadout saves must project the target entity through the typed client."
|
||||
Assert-Contains $inventoryPanel "AetheriaRuntimeStationLoadoutRestoreOption" "InventoryPanel loadout restore submission must use typed station-refit option rows."
|
||||
Assert-Contains $inventoryPanel "TryGetTypedCurrentDockingBayFacade" "InventoryPanel dropdown docking access must be typed-key validated."
|
||||
Assert-Contains $inventoryPanel "selection.EntityKey" "InventoryPanel dropdown selections must carry typed entity keys."
|
||||
Assert-Contains $inventoryPanel "_displayedEntityKey" "InventoryPanel displayed entity state must cache typed entity identity."
|
||||
Assert-Contains $inventoryPanel "_displayedCargoEntityKey" "InventoryPanel displayed cargo state must cache typed cargo identity."
|
||||
Assert-Contains $inventoryPanel "TryResolveStationRefitEntity(string entityKey" "InventoryPanel station-refit selection must resolve from typed entity keys."
|
||||
Assert-NotContains $inventoryPanel "LoadoutTemplatesAsync()" "InventoryPanel must not enumerate loadout templates separately from StationRefitAsync."
|
||||
Assert-NotContains $inventoryPanel "TryResolveLoadoutTemplatePrice" "InventoryPanel must not price loadout restore options locally."
|
||||
Assert-NotContains $inventoryPanel "TryResolveCurrentDockingTargetEntityKey" "InventoryPanel must submit the daemon target key from the typed restore option row."
|
||||
Assert-NotContains $inventoryPanel "TryGetObservedCurrentEntity" "InventoryPanel must not ask ActionGameManager for current-entity identity."
|
||||
Assert-NotContains $inventoryPanel "ObservedLoadoutTemplates" "InventoryPanel must not ask ActionGameManager for loadout templates."
|
||||
Assert-NotContains $inventoryPanel "CreateEntityConstructionBlueprint" "InventoryPanel loadout restore validity must not depend on Unity construction blueprints."
|
||||
Assert-NotContains $inventoryPanel "DockedEntity" "InventoryPanel must not target loadout restore through ActionGameManager.DockedEntity."
|
||||
Assert-NotMatches $inventoryPanel "ExecuteDropdownSelection[\s\S]*?TryGetObservedDockingBay[\s\S]*?private void HideDropdownSurface" "InventoryPanel dropdown selection must not read manager docking-bay identity directly."
|
||||
Assert-NotMatches $inventoryPanel "private AetheriaRuntimeInventoryDropdownSurfaceProjection ProjectDropdownSurface\(\)[\s\S]*?TryGetObservedDockingBay[\s\S]*?private void Update" "InventoryPanel dropdown projection must not read manager docking-bay identity directly."
|
||||
Assert-NotMatches $inventoryPanel "private AetheriaRuntimeInventoryDropdownSurfaceProjection ProjectDropdownSurface\(\)[\s\S]*?TryGetTypedCurrentDockingBayFacade[\s\S]*?private void Update" "InventoryPanel dropdown projection must use typed docking state, not Unity docking-bay facade objects."
|
||||
Assert-NotMatches $inventoryPanel "private bool IsDisplayedEntityKey\([^)]*\)[\s\S]*?TryResolveEntityRecordKey[\s\S]*?private bool IsDisplayedCargoBayKey" "InventoryPanel displayed entity checks must use cached typed identity."
|
||||
Assert-NotMatches $inventoryPanel "private bool IsDisplayedCargoBayKey\([^)]*\)[\s\S]*?TryResolveCargoBay[\s\S]*?private bool TryResolveCargoBay" "InventoryPanel displayed cargo checks must use cached typed identity."
|
||||
Assert-NotMatches $inventoryPanel "ExecuteDropdownSelection[\s\S]*?ObservedAvailableEntities[\s\S]*?private void HideDropdownSurface" "InventoryPanel dropdown selection must resolve options from typed station-refit keys."
|
||||
Assert-NotMatches $inventoryPanel "private AetheriaRuntimeInventoryDropdownSurfaceProjection ProjectDropdownSurface\(\)[\s\S]*?ObservedAvailableEntities[\s\S]*?private void Update" "InventoryPanel dropdown projection must use typed station-refit options."
|
||||
|
||||
$inventoryDropdownSurface = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeInventoryDropdownSurfaceBuilder.cs"
|
||||
Assert-Contains $inventoryDropdownSurface "public string EntityKey { get; }" "Inventory dropdown surface selections must carry typed entity identity."
|
||||
Assert-Contains $inventoryDropdownSurface "entityKey: entity.EntityKey" "Inventory dropdown surface builder must copy typed entity identity into selections."
|
||||
|
||||
$inventoryMenu = Read-Text "Assets\Scripts\UI\Menu\InventoryMenu.cs"
|
||||
Assert-Contains $inventoryMenu "CurrentEntityAsync" "InventoryMenu ship-settings commands must guard against the typed current-entity facade."
|
||||
Assert-Contains $inventoryMenu "TryResolveCurrentEntityDocument" "InventoryMenu ship-settings state must read the typed current-entity document."
|
||||
Assert-Contains $inventoryMenu "_shipSettingsCurrentEntity" "InventoryMenu ship-settings surface must retain typed current-entity state."
|
||||
Assert-Contains $inventoryMenu "latestCurrentEntity.ShutdownPerformance" "InventoryMenu ship-settings surface must use typed shutdown-performance projection."
|
||||
Assert-Contains $inventoryMenu "TryGetTypedCurrentEntityFacade" "InventoryMenu current facade access must be typed-key validated."
|
||||
Assert-Contains $inventoryMenu "SetObservedFacadeIndex(AetheriaUnityObservedFacadeIndex observedFacadeIndex)" "InventoryMenu must receive facade adaptation explicitly."
|
||||
Assert-Contains $inventoryMenu "_observedFacadeIndex.TryResolveEntityByRecordKey" "InventoryMenu must adapt current entity facades by typed daemon record key."
|
||||
Assert-Contains $inventoryMenu "CurrentDockingAsync" "InventoryMenu docking facade access must read the typed current-docking facade."
|
||||
Assert-Contains $inventoryMenu "TryGetTypedCurrentDockingBayFacade" "InventoryMenu docking facade access must be typed-key validated."
|
||||
Assert-Contains $inventoryMenu "ResolveDefaultShutdownPerformance()" "InventoryMenu ship-settings reset must use typed runtime policy for the default threshold."
|
||||
Assert-Contains $inventoryMenu "ResolvePlayerSettings()?.DefaultShutdownPerformance" "InventoryMenu must read default shutdown performance from typed player settings."
|
||||
Assert-NotContains $inventoryMenu "TryGetObservedCurrentEntity(" "InventoryMenu must not read broad manager-global current entity state."
|
||||
Assert-NotContains $inventoryMenu "entity.Settings.ShutdownPerformance" "InventoryMenu ship-settings surface must not read shutdown threshold from a Unity entity facade."
|
||||
Assert-NotContains $inventoryMenu "_shipSettingsEntity" "InventoryMenu ship-settings surface must not store Unity entity facade state."
|
||||
Assert-NotContains $inventoryMenu "GameManager.Settings.GameplaySettings.DefaultShutdownPerformance" "InventoryMenu must not read shutdown defaults from Unity GameSettings."
|
||||
Assert-NotMatches $inventoryMenu "OnEnable\(\)[\s\S]*?TryGetObservedCurrentEntity[\s\S]*?private void OnDisable" "InventoryMenu OnEnable must not read manager current-entity identity directly."
|
||||
Assert-NotMatches $inventoryMenu "OnBackgroundClick[\s\S]*?TryGetObservedCurrentEntity[\s\S]*?private void RenderCurrentShipSettingsSurface" "InventoryMenu background click must not read manager current-entity identity directly."
|
||||
Assert-NotMatches $inventoryMenu "HandleCurrentShipSettingsSurfaceCommand[\s\S]*?TryGetObservedCurrentEntity[\s\S]*?private void HideCurrentShipSettingsSurface" "InventoryMenu ship-settings command handler must not ask ActionGameManager for current-entity identity."
|
||||
Assert-Contains $inventoryPanel "TryResolveTypedInventoryRows" "InventoryPanel cargo/equipment submissions must validate against typed inventory projection rows."
|
||||
Assert-Contains $inventoryPanel "TryValidateTypedCargoSlot" "InventoryPanel cargo submissions must validate typed cargo slot identity."
|
||||
Assert-Contains $inventoryPanel "TryValidateTypedEquipmentSlot" "InventoryPanel equipment submissions must validate typed equipment slot identity."
|
||||
Assert-Contains $inventoryPanel "InventoryAsync(entityIndex)" "InventoryPanel must read non-current inventory through the typed client facade."
|
||||
Assert-Contains $inventoryPanel "origin.Cargo.TryGetValue(item, out var originPosition)" "InventoryPanel cargo submissions must carry the typed origin grid cell."
|
||||
Assert-NotContains $inventoryPanel "int.MinValue" "InventoryPanel cargo/equipment submissions must not send unknown origin coordinates."
|
||||
Assert-NotContains $inventoryPanel "TryResolveObservedCargoBayCommandTarget" "InventoryPanel must not resolve cargo operations through manager command-target adapters."
|
||||
Assert-NotContains $inventoryPanel "TryResolveObservedEquippedItemCommandTarget" "InventoryPanel must not resolve equipment operations through manager command-target adapters."
|
||||
Assert-Contains $inventoryMenu "TryResolveTypedInventoryRows" "InventoryMenu cargo/equipment submissions must validate against typed inventory projection rows."
|
||||
Assert-Contains $inventoryMenu "TryValidateTypedCargoSlot" "InventoryMenu cargo submissions must validate typed cargo slot identity."
|
||||
Assert-Contains $inventoryMenu "TryValidateTypedEquipmentSlot" "InventoryMenu equipment submissions must validate typed equipment slot identity."
|
||||
Assert-Contains $inventoryMenu "InventoryAsync(entityIndex)" "InventoryMenu must read non-current inventory through the typed client facade."
|
||||
Assert-Contains $inventoryMenu "origin.Cargo.TryGetValue(item, out var originPosition)" "InventoryMenu cargo submissions must carry the typed origin grid cell."
|
||||
Assert-Contains $inventoryMenu "SetActionBarPresentation(AetheriaUnityActionBarPresentation actionBarPresentation)" "InventoryMenu must receive action-bar presentation explicitly."
|
||||
Assert-NotContains $inventoryMenu "ProjectEquippedItemActionBarSlots(" "InventoryMenu must not project Unity-local action-bar binding controls into the shared equipped-item surface."
|
||||
Assert-NotContains $inventoryMenu "RequestWeaponGroupActionBarBinding(" "InventoryMenu must not treat shared equipped-item commands as Unity action-bar binding edits."
|
||||
Assert-NotContains $inventoryMenu "RequestClearActionBarBinding(" "InventoryMenu must not treat shared equipped-item commands as Unity action-bar binding clears."
|
||||
Assert-NotContains $inventoryMenu "_actionBarPresentation?.RequestWeaponGroupBinding(slotIndex, groupIndex)" "InventoryMenu must not bind Unity action-bar slots from shared equipped-item commands."
|
||||
Assert-NotContains $inventoryMenu "_actionBarPresentation?.ClearBinding(slotIndex)" "InventoryMenu must not clear Unity action-bar slots from shared equipped-item commands."
|
||||
Assert-NotContains $inventoryMenu "GameManager.GetActionBarSlotCount" "InventoryMenu must not ask ActionGameManager for action-bar slot count."
|
||||
Assert-NotContains $inventoryMenu "GameManager.GetActionBarSlotLabel" "InventoryMenu must not ask ActionGameManager for action-bar slot labels."
|
||||
Assert-NotContains $inventoryMenu "GameManager.GetActionBarBindingLabel" "InventoryMenu must not ask ActionGameManager for action-bar binding labels."
|
||||
Assert-NotContains $inventoryMenu "GameManager.TryResolveActionBarSlotControlPath" "InventoryMenu must not ask ActionGameManager for action-bar control paths."
|
||||
Assert-NotContains $inventoryMenu "int.MinValue" "InventoryMenu cargo/equipment submissions must not send unknown origin coordinates."
|
||||
Assert-NotContains $inventoryMenu "TryResolveObservedCargoBayCommandTarget" "InventoryMenu must not resolve cargo operations through manager command-target adapters."
|
||||
Assert-NotContains $inventoryMenu "TryResolveObservedEquippedItemCommandTarget" "InventoryMenu must not resolve equipment operations through manager command-target adapters."
|
||||
|
||||
$tradeMenu = Read-Text "Assets\Scripts\UI\Menu\TradeMenu.cs"
|
||||
Assert-Contains $tradeMenu "CurrentDockingAsync" "TradeMenu docking and docked-ship purchase access must read the typed current-docking facade."
|
||||
Assert-Contains $tradeMenu "StationRefitAsync" "TradeMenu cargo selector ship options must read the typed station-refit facade."
|
||||
Assert-Contains $tradeMenu "ResolveStationRefit()?.Credits" "TradeMenu credits must read from the typed station-refit projection."
|
||||
Assert-Contains $tradeMenu "BuildStationStockRows" "TradeMenu row list must be built from typed station stock."
|
||||
Assert-Contains $tradeMenu "ResolveStationRefit()?.StationStock" "TradeMenu station stock rows must read from the typed station-refit projection."
|
||||
Assert-Contains $tradeMenu "var sourcePosition = new int2(row.Stock.X, row.Stock.Y)" "TradeMenu trade purchase source position must come from typed station stock."
|
||||
Assert-Contains $tradeMenu "TryResolveCurrentDockingTargetEntityKey" "TradeMenu docked-ship purchase target must resolve from typed current docking."
|
||||
Assert-Contains $tradeMenu "SetTargetCargo" "TradeMenu target cargo selection must store typed entity key and cargo index."
|
||||
Assert-Contains $tradeMenu "OwnedQuantity" "TradeMenu owned counts must read typed station stock ownership facts."
|
||||
Assert-Contains $tradeMenu "CargoTargets" "TradeMenu target cargo rows must come from typed station-refit cargo targets."
|
||||
Assert-Contains $tradeMenu "AetheriaRuntimeStationCargoTargetRow" "TradeMenu cargo selector must use typed station-refit cargo target rows."
|
||||
Assert-NotContains $tradeMenu "ResolveStationStockAdapter" "TradeMenu station stock rows must not adapt Unity cargo items for typed stock display."
|
||||
Assert-NotContains $tradeMenu "Inventory.Cargo.Keys" "TradeMenu station stock display must not enumerate Unity cargo keys."
|
||||
Assert-NotContains $tradeMenu "TryGetTypedCurrentDockingBayFacade" "TradeMenu target cargo selection must not adapt Unity docking bay objects."
|
||||
Assert-NotContains $tradeMenu "TryResolveCargoBay" "TradeMenu purchase target cargo must not resolve through Unity cargo objects."
|
||||
Assert-NotContains $tradeMenu "TryResolveObservedAvailableEntityByKey" "TradeMenu cargo selector must not resolve typed station-refit options through Unity observed entities."
|
||||
Assert-NotContains $tradeMenu "_cargoSelectorStationRefitEntities" "TradeMenu cargo selector must not reconstruct target rows from available entity options."
|
||||
Assert-NotContains $tradeMenu "ObservedAvailableEntities" "TradeMenu cargo selector must not enumerate Unity observed entities."
|
||||
Assert-NotContains $tradeMenu "TryGetObservedDockingBay" "TradeMenu cargo selector must not read manager docking-bay identity."
|
||||
Assert-NotContains $tradeMenu "TryResolveObservedCargoBayCommandTarget" "TradeMenu purchase target cargo must not resolve through manager cargo adapters."
|
||||
Assert-NotContains $tradeMenu "ItemsOfType" "TradeMenu owned counts must not read Unity cargo item dictionaries."
|
||||
Assert-NotContains $tradeMenu "CountAvailablePlayerShips" "TradeMenu must not compute owned ship counts locally."
|
||||
Assert-NotContains $tradeMenu "CountTargetCargoItems" "TradeMenu must not compute owned cargo counts locally."
|
||||
Assert-NotContains $tradeMenu "CargoBays.Count" "TradeMenu cargo selector must not use Unity ship cargo bay counts."
|
||||
Assert-NotContains $tradeMenu "DockedEntity" "TradeMenu must not target purchases through ActionGameManager.DockedEntity."
|
||||
Assert-NotContains $tradeMenu "GameManager.Credits" "TradeMenu must not read manager-global credits."
|
||||
Assert-NotContains $tradeMenu "stationInventory" "TradeMenu must not recover station purchase source through Unity inventory."
|
||||
Assert-NotMatches $tradeMenu "void Populate\(\)[\s\S]*?Inventory\.Cargo\.Keys[\s\S]*?private IEnumerable<TradeRow> BuildStationStockRows" "TradeMenu Populate must not enumerate Unity cargo as station stock truth."
|
||||
Assert-NotMatches $tradeMenu "RequestTradePurchase\([\s\S]*?Cargo\.TryGetValue[\s\S]*?private bool TryResolveCargoBay" "TradeMenu trade purchase command source must not come from Unity cargo lookup."
|
||||
Assert-NotMatches $tradeMenu "OnEnable\(\)[\s\S]*?TryGetObservedDockingBay[\s\S]*?void Populate" "TradeMenu OnEnable must not read manager docking-bay identity directly."
|
||||
Assert-NotMatches $tradeMenu "ProjectTradeCargoSelectorSurface[\s\S]*?TryGetObservedDockingBay[\s\S]*?private void ApplyCargoSelection" "TradeMenu cargo selector projection must not read manager docking-bay identity directly."
|
||||
Assert-NotMatches $tradeMenu "ApplyCargoSelection[\s\S]*?TryGetObservedDockingBay[\s\S]*?private void SetTargetCargo" "TradeMenu cargo selector selection must not read manager docking-bay identity directly."
|
||||
Assert-NotMatches $tradeMenu "ProjectTradeCargoSelectorSurface[\s\S]*?ObservedAvailableEntities[\s\S]*?private void ApplyCargoSelection" "TradeMenu cargo selector projection must use typed station-refit options."
|
||||
Assert-NotMatches $tradeMenu "ApplyCargoSelection[\s\S]*?ObservedAvailableEntities[\s\S]*?private void SetTargetCargo" "TradeMenu cargo selector selection must resolve options from typed station-refit keys."
|
||||
Assert-NotMatches $tradeMenu "private int CountAvailablePlayerShips\([^)]*\)\s*\{[\s\S]*?ObservedAvailableEntities[\s\S]*?\n \}" "TradeMenu available-ship counts must come from typed station-refit state."
|
||||
Assert-NotContains $tradeMenu "HullItemKey" "TradeMenu owned ship counts must come from station stock facts, not UI-side hull comparisons."
|
||||
|
||||
$inventoryPanel = Read-Text "Assets\Scripts\UI\Menu\InventoryPanel.cs"
|
||||
Assert-Contains $inventoryPanel "StationRefitAsync()" "InventoryPanel must resolve station/refit selections from typed station-refit projection."
|
||||
Assert-Contains $inventoryPanel "SetObservedFacadeIndex(AetheriaUnityObservedFacadeIndex observedFacadeIndex)" "InventoryPanel must receive facade adaptation explicitly."
|
||||
Assert-Contains $inventoryPanel "_observedFacadeIndex.TryResolveEntityByRecordKey" "InventoryPanel may adapt Unity facade objects only by typed daemon record key."
|
||||
Assert-Contains $inventoryPanel "_observedFacadeIndex.TryResolveDockingBayByRecordKey" "InventoryPanel must adapt docking bays by typed daemon parent key and docking bay index."
|
||||
Assert-NotContains $inventoryPanel "ObservedAvailableEntities()" "InventoryPanel must not read broad manager-global available entity collections."
|
||||
Assert-NotContains $inventoryPanel "TryGetObservedDockingBay(" "InventoryPanel must read typed current docking before adapting a Unity docking bay facade."
|
||||
|
||||
$inventoryMenu = Read-Text "Assets\Scripts\UI\Menu\InventoryMenu.cs"
|
||||
Assert-Contains $inventoryMenu "CurrentDockingAsync()" "InventoryMenu must resolve docking display state from typed current-docking projection."
|
||||
Assert-Contains $inventoryMenu "_observedFacadeIndex.TryResolveDockingBayByRecordKey" "InventoryMenu must adapt docking bays by typed daemon parent key and docking bay index."
|
||||
Assert-NotContains $inventoryMenu "TryGetObservedDockingBay(" "InventoryMenu must not read broad manager-global docking bay state."
|
||||
|
||||
$localMenu = Read-Text "Assets\Scripts\UI\Menu\LocalMenu.cs"
|
||||
Assert-Contains $localMenu "TryResolveDockedLocalStory(out _currentLocation)" "LocalMenu must resolve local story visibility through its typed docking path."
|
||||
Assert-Contains $localMenu "CurrentDockingAsync()" "LocalMenu must read typed current-docking state before showing local story."
|
||||
Assert-Contains $localMenu "SetObservedFacadeIndex(AetheriaUnityObservedFacadeIndex observedFacadeIndex)" "LocalMenu must receive facade adaptation explicitly."
|
||||
Assert-Contains $localMenu "_observedFacadeIndex.TryResolveDockingBayByRecordKey" "LocalMenu may adapt story-bearing docking parents only by typed parent key and docking bay index."
|
||||
Assert-Contains $localMenu "docking.DockParentEntityKey" "LocalMenu must use typed dock parent identity for local story adaptation."
|
||||
Assert-Contains $localMenu "docking.DockingBayIndex" "LocalMenu must use typed docking bay index for local story adaptation."
|
||||
Assert-NotContains $localMenu "TryGetObservedDockedLocalStory" "LocalMenu must not ask ActionGameManager for docked local story state."
|
||||
Assert-NotContains $localMenu "DockedEntity" "LocalMenu must not read ActionGameManager.DockedEntity."
|
||||
|
||||
$confirmationDialog = Read-Text "Assets\Scripts\UI\ConfirmationDialog.cs"
|
||||
Assert-Contains $confirmationDialog "public void SetInputGate(Action enableGlobalInput, Action disableGlobalInput)" "ConfirmationDialog must receive input gating explicitly."
|
||||
Assert-NotContains $confirmationDialog "ActionGameManager.Instance" "ConfirmationDialog must not reach through ActionGameManager.Instance for input gating."
|
||||
|
||||
$schematicDisplay = Read-Text "Assets\Scripts\UI\HUD\SchematicDisplay.cs"
|
||||
Assert-Contains $schematicDisplay "public void SetRenderSettings(AetheriaRuntimeDaemonRenderSettings renderSettings)" "SchematicDisplay must receive HUD render settings explicitly."
|
||||
Assert-Contains $schematicDisplay "_renderSettings?.NormalizeThermalRisk(" "SchematicDisplay must normalize thermal risk through injected daemon render settings."
|
||||
Assert-Contains $schematicDisplay "private AetheriaRuntimeCurrentEntityDocument _currentEntityDocument;" "SchematicDisplay must cache the typed current-entity HUD document."
|
||||
Assert-Contains $schematicDisplay "ResolveCurrentEntityHudStatus()" "SchematicDisplay must read player HUD facts from typed current-entity state."
|
||||
Assert-Contains $schematicDisplay ".CurrentEntityAsync()" "SchematicDisplay must read current entity HUD status through AetheriaClient."
|
||||
Assert-Contains $schematicDisplay "hud.OverrideShutdown" "SchematicDisplay must render override shutdown from typed HUD state."
|
||||
Assert-Contains $schematicDisplay "hud.HeatsinksEnabled" "SchematicDisplay must render heatsink status from typed HUD state."
|
||||
Assert-Contains $schematicDisplay "hud.Heatstroke" "SchematicDisplay must render heatstroke from typed HUD state."
|
||||
Assert-Contains $schematicDisplay "hud.Hypothermia" "SchematicDisplay must render hypothermia from typed HUD state."
|
||||
Assert-Contains $schematicDisplay "hud.Visibility" "SchematicDisplay must render player visibility from typed HUD state."
|
||||
Assert-NotContains $schematicDisplay "_entity.OverrideShutdown" "SchematicDisplay player HUD must not read override shutdown from the Unity entity facade."
|
||||
Assert-NotContains $schematicDisplay "_entity.HeatsinksEnabled" "SchematicDisplay player HUD must not read heatsink status from the Unity entity facade."
|
||||
Assert-NotContains $schematicDisplay "_entity.Heatstroke" "SchematicDisplay player HUD must not read heatstroke from the Unity entity facade."
|
||||
Assert-NotContains $schematicDisplay "_entity.Hypothermia" "SchematicDisplay player HUD must not read hypothermia from the Unity entity facade."
|
||||
Assert-NotContains $schematicDisplay "ActionGameManager.Instance.ZoneRenderer" "SchematicDisplay must not reach through ActionGameManager.Instance.ZoneRenderer."
|
||||
|
||||
$actionGameManager = Read-Text "Assets\Scripts\Gameplay\ActionGameManager.cs"
|
||||
$observedFrameApplier = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityObservedFrameApplier.cs"
|
||||
$observedTargetQuery = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityObservedTargetQuery.cs"
|
||||
$targetPresentation = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityTargetPresentation.cs"
|
||||
$observedDockingIndex = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityObservedDockingIndex.cs"
|
||||
$currentEntityBinder = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityCurrentEntityBinder.cs"
|
||||
$currentEntityPresentation = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityCurrentEntityPresentation.cs"
|
||||
$menuShell = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityMenuShell.cs"
|
||||
$gameplayInputShell = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityGameplayInputShell.cs"
|
||||
$runtimeClientProvider = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityRuntimeClientProvider.cs"
|
||||
$renderSettingsBridge = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityRenderSettingsBridge.cs"
|
||||
$cockpitHudShell = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityCockpitHudShell.cs"
|
||||
$gameplayBootShell = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityGameplayBootShell.cs"
|
||||
$sceneWiring = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityGameplaySceneWiring.cs"
|
||||
Assert-Contains $actionGameManager "private readonly AetheriaUnityObservedFacadeIndex _observedFacadeIndex" "ActionGameManager must keep observed facade adaptation behind an explicit helper."
|
||||
Assert-Contains $actionGameManager "private AetheriaUnityGameplaySceneWiring SceneWiring =>" "ActionGameManager should delegate Unity scene dependency wiring to a focused helper."
|
||||
Assert-Contains $actionGameManager "SceneWiring.ConfigureObservedFacadeIndex(_observedFacadeIndex)" "ActionGameManager should delegate facade injection to scene wiring."
|
||||
Assert-Contains $sceneWiring "public sealed class AetheriaUnityGameplaySceneWiring" "Unity scene wiring should live outside ActionGameManager."
|
||||
Assert-Contains $sceneWiring "Inventory?.SetObservedFacadeIndex(observedFacadeIndex)" "Scene wiring should inject facade adaptation into InventoryMenu."
|
||||
Assert-NotContains $actionGameManager "public bool TryResolveObservedEntityRecordKey" "ActionGameManager must not expose facade record-key lookup as public manager API."
|
||||
Assert-NotContains $actionGameManager "public bool TryResolveObservedEntityFacadeByRecordKey" "ActionGameManager must not expose entity facade lookup as public manager API."
|
||||
Assert-NotContains $actionGameManager "public bool TryResolveObservedDockingBayFacadeByRecordKey" "ActionGameManager must not expose docking facade lookup as public manager API."
|
||||
Assert-Contains $actionGameManager "var boot = GameplayBootShell.Boot();" "ActionGameManager should delegate runtime boot/catalog setup to the gameplay boot shell."
|
||||
Assert-Contains $actionGameManager "SceneWiring.ConfigureCurrentEntityPresentation(_currentEntityPresentation, boot.RuntimeCatalog);" "ActionGameManager should delegate boot catalog injection into current-entity presentation."
|
||||
Assert-Contains $actionGameManager "SceneWiring.ConfigureTargetPresentation(" "ActionGameManager should delegate boot catalog injection into target presentation."
|
||||
Assert-Contains $actionGameManager "() => ResolveDaemonObserver()?.Client" "ActionGameManager should give target queries the typed local client, not a raw zone snapshot delegate."
|
||||
Assert-Contains $sceneWiring "observedFacadeIndex.TryResolveEntityByDaemonIndex" "Scene wiring should adapt target presentation entities through the facade index."
|
||||
Assert-Contains $actionGameManager "SceneWiring.ConfigureActionBarPresentation(" "ActionGameManager should delegate boot catalog injection into local action-bar presentation."
|
||||
Assert-NotContains $actionGameManager "private void ConfigureCurrentEntityPresentation(" "ActionGameManager must not own current-entity presentation scene wiring."
|
||||
Assert-NotContains $actionGameManager "private void ConfigureTargetPresentation(" "ActionGameManager must not own target presentation scene wiring."
|
||||
Assert-NotContains $actionGameManager "TryGetObservedEntityFacade" "ActionGameManager must not own daemon-index facade lookup shims."
|
||||
Assert-NotContains $actionGameManager "private void ConfigureActionBarPresentation(" "ActionGameManager must not own action-bar presentation scene wiring."
|
||||
Assert-NotContains $actionGameManager "private void ConfigureObservedFacadeIndex(" "ActionGameManager must not own facade scene wiring."
|
||||
Assert-NotContains $actionGameManager "private static AetheriaRuntimeCatalogSnapshot _runtimeCatalog" "ActionGameManager must not cache runtime catalog as manager-global state."
|
||||
Assert-NotContains $actionGameManager "FindTypedRuntimeItem(" "ActionGameManager must not own runtime catalog item lookup policy."
|
||||
Assert-NotContains $actionGameManager "ArticulatedWeaponBehaviorKinds" "ActionGameManager must not own weapon presentation policy."
|
||||
Assert-Contains $actionGameManager "ItemManager = boot.ItemManager;" "ActionGameManager should receive ItemManager from the gameplay boot shell."
|
||||
Assert-Contains $actionGameManager "_loadoutItemProjector = boot.LoadoutItemProjector;" "ActionGameManager should receive loadout projector from the gameplay boot shell."
|
||||
Assert-Contains $gameplayBootShell "public sealed class AetheriaUnityGameplayBootShell" "Unity runtime boot/catalog setup should live outside ActionGameManager."
|
||||
Assert-Contains $gameplayBootShell "AetheriaRuntimeStateBoot.Inspect(AetheriaUnityRuntimePaths.GameDataDirectory)" "Gameplay boot shell should inspect the runtime target."
|
||||
Assert-Contains $gameplayBootShell "AetheriaUnityRuntimeClientProvider.ResolveClient(stateBoot.StateFilePath, stateBoot.RuntimeId)" "Gameplay boot shell should resolve the typed runtime client with the inspected Verse runtime id."
|
||||
Assert-Contains $gameplayBootShell "Aetheria runtime id: {stateBoot.RuntimeId}" "Gameplay boot shell should log the inspected Verse runtime id."
|
||||
Assert-Contains $gameplayBootShell ".OpenRuntimeCatalog()" "Gameplay boot shell should open the typed runtime catalog."
|
||||
Assert-Contains $gameplayBootShell "new ItemManager(" "Gameplay boot shell should construct the Unity item adapter."
|
||||
Assert-Contains $gameplayBootShell "new AetheriaUnityLoadoutItemProjector(itemManager, runtimeCatalog)" "Gameplay boot shell should construct the loadout item projector."
|
||||
Assert-Contains $gameplayBootShell "ZoneRenderer.SetDroppedPickupItemProjector(loadoutItemProjector.CreateLoadoutItem)" "Gameplay boot shell should configure dropped pickup projection."
|
||||
Assert-Contains $gameplayBootShell "ZoneRenderer.BodySettingsCollections = Settings.BodySettingsCollections" "Gameplay boot shell should configure renderer body settings."
|
||||
Assert-Contains $gameplayBootShell "AetheriaUnityRenderSettingsBridge.Build(" "Gameplay boot shell should configure daemon render settings."
|
||||
Assert-Contains $gameplayBootShell "CockpitHudShell.SetRenderSettings(ZoneRenderer.RenderSettings)" "Gameplay boot shell should configure cockpit HUD render settings."
|
||||
Assert-Contains $gameplayBootShell "AetheriaUnityRuntimeClientProvider.PlayerSettings.GraphicsSettings.ShowAsteroidsInMinimap" "Gameplay boot shell should apply minimap asteroid visibility."
|
||||
Assert-Contains $gameplayBootShell "public readonly struct AetheriaUnityGameplayBootResult" "Gameplay boot shell should return the Unity adapters needed by scene composition."
|
||||
Assert-Contains $actionGameManager "UpdatePlayerPanel = () => CockpitHudShell.UpdatePlayer(CurrentEntity)" "ActionGameManager should delegate player HUD refresh to the HUD shell."
|
||||
Assert-Contains $actionGameManager "UpdateTargetPanel = target => CockpitHudShell.UpdateTarget(target, CurrentEntity)" "ActionGameManager should delegate target HUD refresh to the HUD shell."
|
||||
Assert-Contains $cockpitHudShell "public sealed class AetheriaUnityCockpitHudShell" "Cockpit HUD presentation should live outside ActionGameManager."
|
||||
Assert-Contains $cockpitHudShell "public void SetRenderSettings(AetheriaRuntimeDaemonRenderSettings renderSettings)" "Cockpit HUD shell should receive daemon render settings explicitly."
|
||||
Assert-Contains $cockpitHudShell "SchematicDisplay?.SetRenderSettings(renderSettings)" "Cockpit HUD shell should configure the player HUD schematic."
|
||||
Assert-Contains $cockpitHudShell "TargetSchematicDisplay?.SetRenderSettings(renderSettings)" "Cockpit HUD shell should configure the target HUD schematic."
|
||||
Assert-Contains $cockpitHudShell "public void UpdatePlayer(Entity currentEntity)" "Cockpit HUD shell should own player HUD refresh."
|
||||
Assert-Contains $cockpitHudShell "public void UpdateTarget(Entity target, Entity currentEntity)" "Cockpit HUD shell should own target HUD refresh."
|
||||
Assert-NotContains $actionGameManager "ConfigureSchematicDisplayRenderSettings()" "ActionGameManager must not own cockpit HUD render setting configuration."
|
||||
Assert-NotContains $actionGameManager "SchematicDisplay?.SetRenderSettings(" "ActionGameManager must not configure schematic render settings directly."
|
||||
Assert-NotContains $actionGameManager "TargetSchematicDisplay?.SetRenderSettings(" "ActionGameManager must not configure target schematic render settings directly."
|
||||
Assert-NotContains $actionGameManager "private void UpdatePlayerPanel()" "ActionGameManager must not own cockpit HUD player refresh."
|
||||
Assert-NotContains $actionGameManager "private void UpdateTargetPanel(Entity target)" "ActionGameManager must not own cockpit HUD target refresh."
|
||||
Assert-NotContains $actionGameManager "AetheriaRuntimeStateBoot.Inspect(" "ActionGameManager must not inspect runtime targets directly."
|
||||
Assert-NotContains $actionGameManager "ResolveClient(stateBoot.StateFilePath).OpenRuntimeCatalog()" "ActionGameManager must not open runtime catalogs directly."
|
||||
Assert-NotContains $actionGameManager "ItemManager = new ItemManager(" "ActionGameManager must not construct ItemManager directly."
|
||||
Assert-NotContains $actionGameManager "ZoneRenderer.SetDroppedPickupItemProjector(" "ActionGameManager must not configure renderer pickup projection directly."
|
||||
Assert-NotContains $actionGameManager "ZoneRenderer.BodySettingsCollections = Settings.BodySettingsCollections" "ActionGameManager must not configure renderer body settings directly."
|
||||
Assert-Contains $observedFrameApplier "ZoneRenderAsync()" "Unity observed frame application must consume the typed zone-render facade."
|
||||
Assert-NotContains $observedFrameApplier "ProjectZoneRender(" "Unity observed frame application must not rebuild zone-render feeds from raw frames."
|
||||
Assert-NotContains $observedFrameApplier "LastObservedState?.Run" "Unity observed frame application must not read the raw observed run for current-zone lookups."
|
||||
Assert-NotContains $observedFrameApplier "FramePath" "Unity observed frame application should key the presentation cache by typed projection identity, not publication file paths."
|
||||
Assert-Contains $observedTargetQuery "ZoneContactsAsync()" "Unity target queries must consume the typed zone-contacts facade."
|
||||
Assert-NotContains $observedTargetQuery "AetheriaRuntimeZoneSnapshotCommit" "Unity target queries must not receive raw zone snapshots."
|
||||
Assert-NotContains $observedTargetQuery "AetheriaRuntimeDaemonRenderQueries.TryQueryEntityContact" "Unity target queries must not query contacts from raw zone snapshots."
|
||||
Assert-NotContains $observedTargetQuery "AetheriaRuntimeDaemonRenderQueries.TryQueryEntityTarget" "Unity target queries must not query targets from raw zone snapshots."
|
||||
Assert-Contains $targetPresentation "ResolveVisibleContacts" "Unity target presentation must receive typed contact rows."
|
||||
Assert-NotContains $targetPresentation "QueryEntityContacts(" "Unity target presentation must not query contacts from raw zone snapshots."
|
||||
Assert-NotContains $targetPresentation "ResolveZone" "Unity target presentation must not hold raw zone snapshot access."
|
||||
Assert-Contains $observedDockingIndex "CurrentDockingAsync()" "Unity docking state must read the typed current-docking facade."
|
||||
Assert-Contains $observedDockingIndex "TryResolveEntityByRecordKey(docking.DockParentEntityKey" "Unity docking facade adaptation must be keyed by typed dock parent identity."
|
||||
Assert-NotContains $observedDockingIndex "AetheriaRuntimeZoneSnapshotCommit" "Unity docking state must not receive raw zone snapshots."
|
||||
Assert-NotContains $observedDockingIndex "TryGetParentSnapshot" "Unity docking state must not scan raw snapshot parent relationships."
|
||||
Assert-NotContains $observedDockingIndex "TryGetEntitySnapshot" "Unity docking state must not scan raw entity snapshots."
|
||||
Assert-Contains $currentEntityBinder "ResolveZoneRender?.Invoke()" "Current-entity binding may resolve dock camera context through the typed zone-render feed."
|
||||
Assert-NotContains $currentEntityBinder "ResolveDaemonZone" "Current-entity binding must not pass raw zone snapshots into docked presentation."
|
||||
Assert-Contains $currentEntityPresentation "AetheriaRuntimeZoneRenderDocument zoneRender" "Docked presentation must consume the typed zone-render document for body lookups."
|
||||
Assert-Contains $currentEntityPresentation "AetheriaRuntimeZoneRenderBodyPose" "Docked presentation must resolve body look-at through typed render body poses."
|
||||
Assert-NotContains $currentEntityPresentation "AetheriaRuntimeZoneSnapshotCommit daemonZone" "Docked presentation must not receive raw zone snapshots."
|
||||
Assert-Contains $gameplayBootShell "AetheriaUnityRenderSettingsBridge.Build(" "Gameplay boot shell should delegate daemon render-settings lowering to the render settings bridge."
|
||||
Assert-Contains $renderSettingsBridge "public static class AetheriaUnityRenderSettingsBridge" "Unity render settings lowering should live outside ActionGameManager."
|
||||
Assert-Contains $renderSettingsBridge "public static AetheriaRuntimeDaemonRenderSettings Build(" "Unity render settings bridge should expose focused render-settings lowering."
|
||||
Assert-Contains $renderSettingsBridge "new AetheriaRuntimeDaemonRenderSettings(" "Unity render settings bridge should construct the shared daemon render settings."
|
||||
Assert-Contains $renderSettingsBridge "settings.GameplaySettings.HypothermiaTemperature" "Unity render settings bridge should lower thermal HUD thresholds."
|
||||
Assert-Contains $renderSettingsBridge "settings.GameplaySettings.HeatstrokeTemperature" "Unity render settings bridge should lower thermal HUD thresholds."
|
||||
Assert-Contains $renderSettingsBridge "settings.MinimapZoomLevels?.Select(level => (double)level).ToArray()" "Unity render settings bridge should lower minimap zoom levels explicitly."
|
||||
Assert-Contains $actionGameManager "SceneWiring.ConfigureRuntimeInputScreenShell(MenuShell)" "ActionGameManager should delegate runtime input-screen handoff to scene wiring."
|
||||
Assert-Contains $sceneWiring "MainMenu?.SetRuntimeInputScreenShell(menuShell.CanOpenRuntimeInputScreen, menuShell.ShowRuntimeInputScreen)" "Scene wiring should inject Unity menu-shell input-screen callbacks into MainMenu."
|
||||
Assert-Contains $actionGameManager "GameplayInputShell.Bootstrap();" "ActionGameManager should bootstrap Unity gameplay input through the dedicated input shell."
|
||||
Assert-Contains $menuShell "public bool CanOpenRuntimeInputScreen()" "Unity menu shell should own runtime input-screen availability."
|
||||
Assert-Contains $menuShell "public void ShowRuntimeInputScreen()" "Unity menu shell should own runtime input-screen opening."
|
||||
Assert-Contains $gameplayInputShell "public sealed class AetheriaUnityGameplayInputShell" "Unity gameplay input callbacks should live outside ActionGameManager."
|
||||
Assert-Contains $gameplayInputShell "Input.Global.InputScreen.performed += context => MenuShell.ToggleFullscreenMenu(MenuShell.HelpScreen);" "Gameplay input shell should route the runtime input-screen hotkey through the shared menu shell."
|
||||
Assert-Contains $gameplayInputShell "Input.Player.TargetNearest.performed += context => PilotOperationAdapter.RequestTargetNearest();" "Gameplay input shell should lower player targeting input through typed pilot operations."
|
||||
Assert-Contains $gameplayInputShell "private void RegisterActionBarInput()" "Gameplay input shell should own action-bar control registration."
|
||||
Assert-Contains $gameplayInputShell "DragSession.RegisterTarget(dragAction => ActionBarPresentation.RequestBinding(slot, dragAction));" "Gameplay input shell should wire action-bar drag targets through injected presentation."
|
||||
Assert-NotContains $actionGameManager "private bool CanOpenRuntimeInputScreen()" "ActionGameManager should not own runtime input-screen availability."
|
||||
Assert-NotContains $actionGameManager "private void ShowRuntimeInputScreen()" "ActionGameManager should not own runtime input-screen opening."
|
||||
Assert-NotContains $actionGameManager "Input.Global.InputScreen.performed += context" "ActionGameManager should not own gameplay input callbacks."
|
||||
Assert-NotContains $actionGameManager "Input.Global.ZoneMap.performed += context" "ActionGameManager should not own gameplay input callbacks."
|
||||
Assert-NotContains $actionGameManager "Input.Player.TargetNearest.performed += context" "ActionGameManager should not own player targeting callbacks."
|
||||
Assert-NotContains $actionGameManager "new InputAction(binding: controlPath)" "ActionGameManager should not own action-bar input action construction."
|
||||
Assert-NotContains $actionGameManager "OnPointerEnterAsObservable()" "ActionGameManager should not own action-bar drag target wiring."
|
||||
Assert-NotContains $actionGameManager "private void RequestDock()" "ActionGameManager should not keep gameplay request pass-through wrappers."
|
||||
Assert-NotContains $actionGameManager "private void RequestUndock()" "ActionGameManager should not keep gameplay request pass-through wrappers."
|
||||
Assert-NotContains $actionGameManager "private void RequestInteract()" "ActionGameManager should not keep gameplay request pass-through wrappers."
|
||||
Assert-NotContains $actionGameManager "private void RequestTowToStation()" "ActionGameManager should not keep gameplay request pass-through wrappers."
|
||||
Assert-NotContains $actionGameManager "_towingStation" "ActionGameManager should not cache stale towing context."
|
||||
Assert-NotContains $actionGameManager "UpdateTowingStation" "ActionGameManager should not receive stale towing context callbacks."
|
||||
Assert-Contains $actionGameManager "AetheriaUnityObservedRunProjection.FindZone" "ActionGameManager must not own observed galaxy zone lookup logic."
|
||||
Assert-Contains $actionGameManager "() => AetheriaUnityObservedRunProjection.Galaxy" "ActionGameManager may pass the observed sector projection only into the quarantined zone context adapter."
|
||||
Assert-NotContains $actionGameManager "private static Galaxy ObservedGalaxy" "ActionGameManager must not keep an observed galaxy property."
|
||||
Assert-NotContains $actionGameManager "FindObservedGalaxyZone" "ActionGameManager must not own observed galaxy zone lookup logic."
|
||||
Assert-NotContains $actionGameManager "public static void ProjectObservedDaemonRun(" "ActionGameManager must not own daemon run projection."
|
||||
Assert-Contains $mainMenu "AetheriaUnityObservedRunProjection.Project(" "MainMenu must project daemon run boot data through the explicit observed-run projection holder."
|
||||
Assert-NotContains $actionGameManager "public bool CanShowInputScreenFromMenu()" "ActionGameManager must not expose input-screen availability as public manager API."
|
||||
Assert-NotContains $actionGameManager "public void ShowInputScreenFromMenu()" "ActionGameManager must not expose input-screen opening as public manager API."
|
||||
Assert-NotContains $actionGameManager "public static Galaxy ObservedGalaxy" "ActionGameManager must not expose the observed galaxy as public gameplay state."
|
||||
Assert-NotContains $actionGameManager "public static bool IsTutorial" "ActionGameManager must not mirror tutorial mode as public manager-global gameplay state."
|
||||
Assert-NotContains $actionGameManager "CurrentEnvironment" "ActionGameManager must not expose presentation environment as manager-global gameplay state."
|
||||
Assert-NotContains $actionGameManager "TryGetObservedGalaxy(out Galaxy galaxy)" "ActionGameManager must not expose broad observed galaxy read shims."
|
||||
Assert-NotContains $actionGameManager "TryGetObservedZoneSnapshot(" "ActionGameManager must not expose broad observed zone snapshot read shims."
|
||||
Assert-NotContains $actionGameManager "TryGetObservedRunZone(" "ActionGameManager must not expose broad observed run-zone read shims."
|
||||
Assert-NotContains $actionGameManager "public EquippedDockingBay DockingBay" "ActionGameManager must not expose renderer-local docking bay state as public gameplay truth."
|
||||
Assert-NotContains $actionGameManager "public Entity DockedEntity" "ActionGameManager must not expose renderer-local docked entity state as public gameplay truth."
|
||||
Assert-NotContains $actionGameManager "public Entity TowingStation" "ActionGameManager must not expose renderer-local towing context as public gameplay truth."
|
||||
Assert-NotContains $actionGameManager "AvailableCargoBays()" "ActionGameManager must not expose renderer-local available cargo bay enumeration."
|
||||
Assert-NotContains $actionGameManager "public int Credits" "ActionGameManager must not expose manager-global credits."
|
||||
Assert-NotContains $actionGameManager "Credits = render.Credits" "ActionGameManager must not mirror daemon credits into manager-global state."
|
||||
Assert-NotContains $actionGameManager "public static AetheriaRuntimeCatalogSnapshot RuntimeCatalog" "ActionGameManager must not expose the runtime catalog as manager-global gameplay state."
|
||||
Assert-NotContains $actionGameManager "public AetheriaRuntimeCatalogSnapshot RuntimeCatalog" "ActionGameManager must not expose the runtime catalog as manager-global gameplay state."
|
||||
Assert-NotContains $actionGameManager "ObservedDaemonRenderSettings(" "ActionGameManager must not own daemon render settings lowering."
|
||||
Assert-NotContains $actionGameManager "new AetheriaRuntimeDaemonRenderSettings(" "ActionGameManager must not construct daemon render settings directly."
|
||||
Assert-NotContains $actionGameManager "public EntityConstructionBlueprint CreateEntityConstructionBlueprint(AetheriaRuntimeLoadoutTemplateSnapshot template)" "ActionGameManager loadout template lowering must remain private renderer/bootstrap adaptation."
|
||||
Assert-NotContains $actionGameManager "public static RuntimePlayerSettings RuntimePlayerSettings" "ActionGameManager must not expose player settings as manager-global gameplay state."
|
||||
Assert-Contains $actionGameManager "AetheriaUnityRuntimeClientProvider.PlayerSettings" "ActionGameManager should read Unity runtime player settings through the runtime client provider."
|
||||
Assert-Contains $gameplayBootShell "AetheriaUnityRuntimeClientProvider.ResolveClient(stateBoot.StateFilePath, stateBoot.RuntimeId)" "Gameplay boot shell should open typed catalog through the runtime client provider."
|
||||
Assert-Contains $runtimeClientProvider "public static class AetheriaUnityRuntimeClientProvider" "Unity runtime client cache should live outside ActionGameManager."
|
||||
Assert-Contains $runtimeClientProvider "public static RuntimePlayerSettings PlayerSettings =>" "Unity runtime client provider should own player settings loading."
|
||||
Assert-Contains $runtimeClientProvider "public static AetheriaClient ResolveClient(string stateFilePath, string runtimeId = """")" "Unity runtime client provider should own runtime-id-aware typed client caching."
|
||||
Assert-Contains $runtimeClientProvider "PlayerSettingsAsync()" "Unity runtime client provider should read player settings through AetheriaClient."
|
||||
Assert-NotContains $actionGameManager "ResolveRuntimeClient(" "ActionGameManager should not own typed client caching."
|
||||
Assert-NotContains $actionGameManager "PlayerSettingsAsync()" "ActionGameManager should not load player settings directly."
|
||||
Assert-NotContains $actionGameManager "ApplyRuntimePlayerSettings(" "ActionGameManager should not map player settings snapshots."
|
||||
Assert-NotContains $actionGameManager "_runtimeClient" "ActionGameManager should not own typed runtime client cache."
|
||||
Assert-NotContains $actionGameManager "public static DirectoryInfo GameDataDirectory" "ActionGameManager must not expose Unity runtime paths as gameplay state."
|
||||
Assert-NotContains $actionGameManager "public static string RuntimeStateFilePath" "ActionGameManager must not expose Unity runtime paths as gameplay state."
|
||||
Assert-NotContains $actionGameManager "ObservedAvailableEntities()" "ActionGameManager must not expose broad observed available entity collections as station/refit truth."
|
||||
Assert-NotContains $actionGameManager "TryResolveObservedCargoBayCommandTarget" "ActionGameManager must not expose cargo command-target adapters."
|
||||
Assert-NotContains $actionGameManager "TryResolveObservedEquippedItemCommandTarget" "ActionGameManager must not expose equipment command-target adapters."
|
||||
Assert-NotContains $actionGameManager "TryResolveCargoBayCommandTarget" "ActionGameManager must not keep private cargo command-target adapters."
|
||||
Assert-NotContains $actionGameManager "TryResolveEquippedItemCommandTarget" "ActionGameManager must not keep private equipment command-target adapters."
|
||||
Assert-NotContains $actionGameManager "TryGetObservedDockedLocalStory" "ActionGameManager must not expose docked local-story adapters."
|
||||
Assert-NotContains $actionGameManager "IsObservedDocked" "ActionGameManager must not expose docked-state read shims."
|
||||
Assert-NotContains $actionGameManager "TryGetObservedCurrentEntity(" "ActionGameManager must not expose broad current-entity read shims."
|
||||
Assert-NotContains $actionGameManager "TryGetObservedDockingBay(" "ActionGameManager must not expose broad docking-bay read shims."
|
||||
Assert-NotContains $actionGameManager "ObservedLoadoutTemplates" "ActionGameManager must not expose loadout templates as manager-global observed state."
|
||||
Assert-NotContains $actionGameManager "_observedLoadoutTemplates" "ActionGameManager must not cache loadout templates as manager-global observed state."
|
||||
Assert-NotContains $actionGameManager "LoadRuntimeLoadoutTemplates" "ActionGameManager must not preload loadout templates; clients read typed state directly."
|
||||
Assert-NotContains $actionGameManager "ProjectLoadoutTemplate(Entity entity)" "ActionGameManager must not own typed loadout projection."
|
||||
Assert-NotContains $actionGameManager "public DragObject DragObject" "ActionGameManager must not expose raw drag gesture state."
|
||||
Assert-NotContains $actionGameManager "public bool HasDragTarget" "ActionGameManager must not expose raw drag-target state."
|
||||
Assert-NotContains $actionGameManager "_dragObject" "ActionGameManager must not own raw drag gesture fields."
|
||||
Assert-NotContains $actionGameManager "_endDragCallback" "ActionGameManager must not own raw drag-target callback fields."
|
||||
Assert-Contains $actionGameManager "private readonly AetheriaUnityDragSession _dragSession" "ActionGameManager should delegate drag session state to a helper."
|
||||
Assert-Contains $actionGameManager "SceneWiring.ConfigureInventoryDragSession(_dragSession)" "ActionGameManager should delegate drag-session UI injection to scene wiring."
|
||||
Assert-Contains $sceneWiring "Inventory?.SetDragSession(dragSession)" "InventoryMenu should receive the shared drag session explicitly."
|
||||
Assert-NotContains $actionGameManager "public void BeginDrag(" "ActionGameManager must not expose inventory drag begin as manager API."
|
||||
Assert-NotContains $actionGameManager "public bool TryGetDraggedItem(" "ActionGameManager must not expose inventory drag queries as manager API."
|
||||
Assert-NotContains $actionGameManager "public void RegisterDragTarget(" "ActionGameManager must not expose inventory drag target registration as manager API."
|
||||
Assert-NotContains $actionGameManager "public void UnregisterDragTarget(" "ActionGameManager must not expose inventory drag target clearing as manager API."
|
||||
Assert-NotContains $actionGameManager "public bool EndDrag(" "ActionGameManager must not expose inventory drag completion as manager API."
|
||||
|
||||
$dragSession = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityDragSession.cs"
|
||||
$dragObjects = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityDragObjects.cs"
|
||||
Assert-Contains $dragSession "public sealed class AetheriaUnityDragSession" "Inventory drag session state should live outside ActionGameManager."
|
||||
Assert-Contains $dragSession "private DragObject _dragObject" "Drag session helper should own the raw drag object."
|
||||
Assert-Contains $dragSession "private Action<DragObject> _endDragCallback" "Drag session helper should own the drop target callback."
|
||||
Assert-Contains $dragObjects "public abstract class DragObject" "Unity drag payload objects should live outside ActionGameManager."
|
||||
Assert-Contains $dragObjects "public abstract class ItemDragObject : DragObject" "Unity item drag payload objects should live outside ActionGameManager."
|
||||
Assert-Contains $dragObjects "public class ItemInstanceDragObject : ItemDragObject" "Unity item-instance drag payload objects should live outside ActionGameManager."
|
||||
Assert-Contains $dragObjects "public class EquippedItemDragObject : ItemDragObject" "Unity equipped-item drag payload objects should live outside ActionGameManager."
|
||||
Assert-NotContains $actionGameManager "public abstract class DragObject" "ActionGameManager must not define raw drag payload objects."
|
||||
Assert-NotContains $actionGameManager "public abstract class ItemDragObject" "ActionGameManager must not define raw drag payload objects."
|
||||
Assert-NotContains $actionGameManager "public class ItemInstanceDragObject" "ActionGameManager must not define raw drag payload objects."
|
||||
Assert-NotContains $actionGameManager "public class EquippedItemDragObject" "ActionGameManager must not define raw drag payload objects."
|
||||
|
||||
$actionBarSlot = Read-Text "Assets\Scripts\Gameplay\ActionBarSlot.cs"
|
||||
Assert-Contains $actionBarSlot "protected GameSettings Settings { get; }" "ActionBar bindings should receive presentation settings explicitly."
|
||||
Assert-Contains $actionBarSlot "Settings.GetIcon(weaponType)" "ActionBar gear icon fallback should use injected presentation settings."
|
||||
Assert-NotContains $actionBarSlot "ActionGameManager.Instance" "ActionBarSlot must not reach through ActionGameManager.Instance for icon presentation."
|
||||
|
||||
$actionBarPresentation = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityActionBarPresentation.cs"
|
||||
Assert-Contains $actionBarPresentation "public sealed class AetheriaUnityActionBarPresentation" "Action-bar presentation reads should live outside ActionGameManager."
|
||||
Assert-Contains $actionBarPresentation "public int SlotCount" "Action-bar presentation should expose slot count without manager API."
|
||||
Assert-Contains $actionBarPresentation "public string SlotLabel(int slotIndex)" "Action-bar presentation should expose slot labels without manager API."
|
||||
Assert-Contains $actionBarPresentation "public string BindingLabel(int slotIndex)" "Action-bar presentation should expose binding labels without manager API."
|
||||
Assert-Contains $actionBarPresentation "public bool TryResolveControlPath(int slotIndex, out string controlPath)" "Action-bar presentation should resolve slot control paths without manager API."
|
||||
Assert-Contains $actionBarPresentation "public bool RequestWeaponGroupBinding(int slotIndex, int groupIndex)" "Action-bar presentation should own local weapon-group binding edits."
|
||||
Assert-Contains $actionBarPresentation "public bool ClearBinding(int slotIndex)" "Action-bar presentation should own local binding clears."
|
||||
Assert-Contains $actionBarPresentation "ApplyLocalBindings();" "Action-bar presentation should apply local input mappings directly."
|
||||
Assert-NotContains $actionBarPresentation "Operations.SetActionBarBinding(" "Action-bar binding edits are Unity-local input maps, not daemon operations."
|
||||
Assert-NotContains $actionBarPresentation "Operations.ClearActionBarBinding(" "Action-bar binding clears are Unity-local input maps, not daemon operations."
|
||||
Assert-Contains $actionGameManager "private readonly AetheriaUnityActionBarPresentation _actionBarPresentation" "ActionGameManager should own only the local action-bar presentation wiring."
|
||||
Assert-Contains $sceneWiring "public void ConfigureActionBarPresentation(" "Scene wiring should inject action-bar presentation into UI shells with explicit boot catalog."
|
||||
Assert-Contains $sceneWiring "Inventory?.SetActionBarPresentation(actionBarPresentation)" "InventoryMenu should receive action-bar presentation through scene wiring."
|
||||
Assert-Contains $actionGameManager "ActionBarPresentation = _actionBarPresentation" "ActionGameManager should inject action-bar presentation into the input shell."
|
||||
Assert-Contains $sceneWiring "gameplayInputShell?.ActionBarSlots" "Scene wiring should bind action-bar presentation to input-shell slots."
|
||||
Assert-NotContains $actionGameManager "public int GetActionBarSlotCount(" "ActionGameManager must not expose action-bar slot count as public UI API."
|
||||
Assert-NotContains $actionGameManager "public string GetActionBarSlotLabel(" "ActionGameManager must not expose action-bar slot labels as public UI API."
|
||||
Assert-NotContains $actionGameManager "public string GetActionBarBindingLabel(" "ActionGameManager must not expose action-bar binding labels as public UI API."
|
||||
Assert-NotContains $actionGameManager "public bool TryResolveActionBarSlotControlPath(" "ActionGameManager must not expose action-bar control path resolution as public UI API."
|
||||
|
||||
$laser = Read-Text "Assets\Scripts\Gameplay\Weapons\Laser.cs"
|
||||
$constantLaser = Read-Text "Assets\Scripts\Gameplay\Weapons\ConstantLaser.cs"
|
||||
$hitscanEffect = Read-Text "Assets\Scripts\Gameplay\Weapons\HitscanEffect.cs"
|
||||
$guidedProjectile = Read-Text "Assets\Scripts\Gameplay\Weapons\GuidedProjectile.cs"
|
||||
$laserManager = Read-Text "Assets\Scripts\Gameplay\Weapons\LaserManager.cs"
|
||||
$constantLaserManager = Read-Text "Assets\Scripts\Gameplay\Weapons\ConstantLaserManager.cs"
|
||||
$hitscanManager = Read-Text "Assets\Scripts\Gameplay\Weapons\HitscanManager.cs"
|
||||
$guidedProjectileManager = Read-Text "Assets\Scripts\Gameplay\Weapons\GuidedProjectileManager.cs"
|
||||
Assert-Contains $laser "public ZoneRenderer ZoneRenderer { get; set; }" "Laser effects must receive renderer context explicitly for Ymir queries."
|
||||
Assert-Contains $constantLaser "public ZoneRenderer ZoneRenderer { get; set; }" "Constant laser effects must receive renderer context explicitly for Ymir queries."
|
||||
Assert-Contains $hitscanEffect "public ZoneRenderer ZoneRenderer { get; set; }" "Hitscan effects must receive renderer context explicitly for Ymir queries."
|
||||
Assert-Contains $guidedProjectile "public ZoneRenderer ZoneRenderer { get; set; }" "Guided projectiles must receive renderer context explicitly for Ymir queries."
|
||||
Assert-Contains $guidedProjectile "child.ZoneRenderer = ZoneRenderer" "Guided projectile children must inherit explicit renderer context."
|
||||
Assert-Contains $laserManager "p.ZoneRenderer = source.ZoneRenderer" "LaserManager must pass source renderer context to effects."
|
||||
Assert-Contains $constantLaserManager "p.ZoneRenderer = source.ZoneRenderer" "ConstantLaserManager must pass source renderer context to effects."
|
||||
Assert-Contains $hitscanManager "p.ZoneRenderer = source.ZoneRenderer" "HitscanManager must pass source renderer context to effects."
|
||||
Assert-Contains $guidedProjectileManager "p.ZoneRenderer = source.ZoneRenderer" "GuidedProjectileManager must pass source renderer context to effects."
|
||||
Assert-NotContains $laser "ActionGameManager.Instance" "Laser effects must not reach through ActionGameManager.Instance."
|
||||
Assert-NotContains $constantLaser "ActionGameManager.Instance" "Constant laser effects must not reach through ActionGameManager.Instance."
|
||||
Assert-NotContains $hitscanEffect "ActionGameManager.Instance" "Hitscan effects must not reach through ActionGameManager.Instance."
|
||||
Assert-NotContains $guidedProjectile "ActionGameManager.Instance" "Guided projectiles must not reach through ActionGameManager.Instance."
|
||||
|
||||
$client = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaClient.cs"
|
||||
$loadoutProjector = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeLoadoutSnapshotProjector.cs"
|
||||
Assert-Contains $loadoutProjector "ProjectLoadoutTemplate(" "Typed loadout projection must live in the shared runtime package."
|
||||
Assert-Contains $loadoutProjector "AetheriaRuntimeEntitySnapshotCommit entity" "Shared loadout projection must support daemon entity snapshots."
|
||||
Assert-Contains $client "LoadoutTemplateAsync" "AetheriaClient must expose daemon-backed loadout template projection."
|
||||
|
||||
$inventoryPanel = Read-Text "Assets\Scripts\UI\Menu\InventoryPanel.cs"
|
||||
Assert-NotContains $inventoryPanel "AetheriaRuntimeLoadoutProjector.ProjectLoadoutTemplate(entity)" "InventoryPanel must not save loadouts by serializing Unity entity facades."
|
||||
Assert-NotContains $inventoryPanel "ActionGameManager.ProjectLoadoutTemplate" "InventoryPanel must not ask ActionGameManager to project typed loadouts."
|
||||
Assert-Contains $inventoryPanel "public void SetDragSession(AetheriaUnityDragSession dragSession)" "InventoryPanel must receive drag state as an explicit UI dependency."
|
||||
Assert-Contains $inventoryPanel "DragSession.TryGetDraggedItem(out var itemDragObject)" "InventoryPanel must query item drags through the injected drag session."
|
||||
Assert-NotContains $inventoryPanel "GameManager.BeginDrag" "InventoryPanel must not route drag begin through ActionGameManager."
|
||||
Assert-NotContains $inventoryPanel "GameManager.EndDrag" "InventoryPanel must not route drag completion through ActionGameManager."
|
||||
Assert-NotContains $inventoryPanel "GameManager.TryGetDraggedItem" "InventoryPanel must not query drag state through ActionGameManager."
|
||||
Assert-NotContains $inventoryPanel "GameManager.RegisterDragTarget" "InventoryPanel must not route drag target registration through ActionGameManager."
|
||||
Assert-NotContains $inventoryPanel "GameManager.UnregisterDragTarget" "InventoryPanel must not route drag target clearing through ActionGameManager."
|
||||
Assert-NotContains $inventoryPanel "GameManager.DragObject" "InventoryPanel must not peek raw manager drag state."
|
||||
Assert-NotContains $inventoryPanel "GameManager.HasDragTarget" "InventoryPanel must not peek raw manager drag-target state."
|
||||
|
||||
$runtimePaths = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityRuntimePaths.cs"
|
||||
Assert-Contains $runtimePaths "public static class AetheriaUnityRuntimePaths" "Unity runtime paths must live outside ActionGameManager."
|
||||
Assert-Contains $runtimePaths "public static DirectoryInfo GameDataDirectory" "Unity runtime paths must expose the GameData directory through a dedicated helper."
|
||||
Assert-Contains $runtimePaths "public static string RuntimeStateFilePath" "Unity runtime paths must expose the state file path through a dedicated helper."
|
||||
|
||||
$stateDocs = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonDocuments.cs"
|
||||
Assert-Contains $stateDocs "ObjectsViewport" "Daemon schema catalog must advertise objects viewport."
|
||||
Assert-Contains $stateDocs "GravityViewport" "Daemon schema catalog must advertise gravity viewport."
|
||||
Assert-Contains $stateDocs "CurrentZone" "Daemon schema catalog must advertise current zone."
|
||||
Assert-Contains $stateDocs "CurrentEntity" "Daemon schema catalog must advertise current entity."
|
||||
Assert-Contains $stateDocs "CurrentDocking" "Daemon schema catalog must advertise current docking."
|
||||
Assert-Contains $stateDocs "ZoneContacts" "Daemon schema catalog must advertise zone contacts."
|
||||
Assert-Contains $stateDocs "StationRefit" "Daemon schema catalog must advertise station refit."
|
||||
Assert-Contains $stateDocs "SectorMap" "Daemon schema catalog must advertise sector map."
|
||||
Assert-Contains $stateDocs "ZoneDetails" "Daemon schema catalog must advertise zone details."
|
||||
Assert-Contains $stateDocs "ZoneRender" "Daemon schema catalog must advertise zone render feed."
|
||||
|
||||
$catalogStore = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeCatalogStore.cs"
|
||||
Assert-Contains $catalogStore "ProjectTradeValuePolicySurfaceDocument" "Catalog store must expose typed trade-value policy as a designer Eve surface."
|
||||
Assert-Contains $catalogStore "AetheriaRuntimeTradeValuePolicySurfaceBuilder.Build" "Catalog store must build trade-value policy through the shared Eve surface builder."
|
||||
|
||||
$tradeValuePolicySurface = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeTradeValuePolicySurfaceBuilder.cs"
|
||||
Assert-Contains $tradeValuePolicySurface "SurfaceId = ""aetheria.tradeValuePolicy""" "Trade-value policy surface must have a stable designer surface id."
|
||||
Assert-Contains $tradeValuePolicySurface "Quality Price Modifier" "Trade-value policy surface must expose the quality pricing curve."
|
||||
Assert-Contains $tradeValuePolicySurface "Rarity Tiers" "Trade-value policy surface must expose rarity tiers."
|
||||
Assert-Contains $tradeValuePolicySurface "AetheriaRuntimeTradeValuePolicyCommands.SetQualityMinimum" "Trade-value policy surface must expose typed curve edit commands."
|
||||
Assert-Contains $tradeValuePolicySurface "AetheriaRuntimeTradeValuePolicyCommands.SetTierQuality" "Trade-value policy surface must expose typed tier edit commands."
|
||||
Assert-Contains $tradeValuePolicySurface "control.text" "Trade-value policy surface must expose editable numeric controls instead of read-only metrics only."
|
||||
|
||||
$eveCommandDocument = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEveCommandDocument.cs"
|
||||
Assert-Contains $eveCommandDocument "AetheriaRuntimeTradeValuePolicyCommandBody" "Eve command document must carry a typed trade-value policy command body."
|
||||
Assert-Contains $eveCommandDocument "SetTradeValueQualityMinimum" "Eve command kinds must include typed trade-value policy edits."
|
||||
|
||||
$eveCommandClient = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEveCommandClient.cs"
|
||||
Assert-Contains $eveCommandClient "CreateTradeValuePolicyCommand" "Eve command client must lower trade-value policy surface requests into typed commands."
|
||||
Assert-Contains $eveCommandClient "ReadTradeValuePolicyBody" "Eve command client must parse trade-value policy payload into a typed body."
|
||||
|
||||
$eveCommandBridge = Read-Text "Aetheria.State\AetheriaEveCommandBridge.cs"
|
||||
Assert-Contains $eveCommandBridge "ExecuteTradeValuePolicyCommandAsync" "Eve command bridge must persist typed trade-value policy edits."
|
||||
Assert-Contains $eveCommandBridge "PutTradeValuePolicyAsync" "Trade-value policy edits must write the typed policy document."
|
||||
|
||||
$tickRunner = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonTickRunner.cs"
|
||||
Assert-Contains $tickRunner "ProjectTradeValuePolicySurfaceDocument(stateFilePath)" "Daemon editor surface must publish the trade-value policy designer surface."
|
||||
|
||||
$client = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaClient.cs"
|
||||
Assert-Contains $client "ObjectsViewportAsync" "AetheriaClient must expose objects viewport facade."
|
||||
Assert-Contains $client "GravityViewportAsync" "AetheriaClient must expose gravity viewport facade."
|
||||
Assert-Contains $client "CurrentZoneAsync" "AetheriaClient must expose current-zone facade."
|
||||
Assert-Contains $client "CurrentEntityAsync" "AetheriaClient must expose current-entity facade."
|
||||
Assert-Contains $client "CurrentDockingAsync" "AetheriaClient must expose current-docking facade."
|
||||
Assert-Contains $client "ZoneContactsAsync" "AetheriaClient must expose zone-contacts facade."
|
||||
Assert-Contains $client "StationRefitAsync" "AetheriaClient must expose station-refit facade."
|
||||
Assert-Contains $client "SectorMapAsync" "AetheriaClient must expose sector-map facade."
|
||||
Assert-Contains $client "ZoneDetailsAsync" "AetheriaClient must expose zone-details facade."
|
||||
Assert-Contains $client "ZoneRenderAsync" "AetheriaClient must expose zone-render facade."
|
||||
|
||||
$projection = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeRtsProjection.cs"
|
||||
Assert-Contains $projection "ProjectZoneDetails" "Shared projection code must own the zone-details feed."
|
||||
Assert-Contains $projection "ProjectZoneContacts" "Shared projection code must own the zone-contacts feed."
|
||||
Assert-Contains $projection "ProjectZoneTargetRow" "Zone-contacts projection must carry target geometry rows."
|
||||
Assert-Contains $projection "Distance = Math.Sqrt(deltaX * deltaX + deltaZ * deltaZ)" "Zone-contacts projection must carry target/contact distances."
|
||||
Assert-Contains $projection "ProjectZoneRender" "Shared projection code must own the zone-render feed."
|
||||
Assert-Contains $projection "ProjectCurrentEntity" "Shared projection code must own the current-entity feed."
|
||||
Assert-Contains $projection "ProjectCurrentDocking" "Shared projection code must own the current-docking feed."
|
||||
Assert-Contains $projection "ProjectStationRefit" "Shared projection code must own the station-refit feed."
|
||||
Assert-Contains $projection "Credits = context.Run.Credits" "Station-refit projection must carry typed run credits."
|
||||
Assert-Contains $projection "ProjectStationStock" "Station-refit projection must carry typed station stock."
|
||||
Assert-Contains $projection "CargoItems = ProjectStationStock(entity)" "Station-refit entity options must carry typed cargo rows for target counts."
|
||||
Assert-Contains $projection "ProjectStationStockTradeFacts" "Station-refit projection must enrich station stock with trade facts."
|
||||
Assert-Contains $projection "OwnedQuantity = CountStationOwnedQuantity" "Station-refit projection must publish owned counts for trade rows."
|
||||
Assert-Contains $projection "CanAfford = price >= 0 && credits >= price" "Station-refit projection must publish affordability for trade rows."
|
||||
Assert-Contains $projection "SourceIndex = sourceIndex" "Inventory projections must carry source slot identity for cargo/equipment operations."
|
||||
Assert-Contains $projection "X = slot.X" "Inventory projections must carry item grid X for cargo/equipment operations."
|
||||
Assert-Contains $projection "Y = slot.Y" "Inventory projections must carry item grid Y for cargo/equipment operations."
|
||||
|
||||
$zoneRenderDocs = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeRtsViewportDocuments.cs"
|
||||
Assert-Contains $zoneRenderDocs "public int SourceIndex" "Inventory item projection documents must carry source index."
|
||||
Assert-Contains $zoneRenderDocs "public int X" "Inventory item projection documents must carry grid X."
|
||||
Assert-Contains $zoneRenderDocs "public int Y" "Inventory item projection documents must carry grid Y."
|
||||
Assert-Contains $zoneRenderDocs "AetheriaRuntimeZoneRenderDocument" "Runtime documents must define the zone-render feed."
|
||||
Assert-Contains $zoneRenderDocs "AetheriaRuntimeZoneDetailsDocument" "Runtime documents must define the zone-details feed."
|
||||
Assert-Contains $zoneRenderDocs "public bool IsTutorial" "Sector-map feed must carry tutorial mode for observed Unity boot without whole-frame reads."
|
||||
Assert-Contains $zoneRenderDocs "public uint GenerationSeed" "Sector-map feed must carry generation seed for observed Unity boot without whole-frame reads."
|
||||
Assert-Contains $zoneRenderDocs "FactionRelationships" "Sector-map feed must carry faction relationship facts for observed Unity boot without whole-frame reads."
|
||||
Assert-Contains $zoneRenderDocs "EntityHullItemKeys" "Zone-details feed must carry entity hull identity without exposing whole zone snapshots."
|
||||
Assert-Contains $zoneRenderDocs "AetheriaRuntimeZoneContactsDocument" "Runtime documents must define the zone-contacts feed."
|
||||
Assert-Contains $zoneRenderDocs "AetheriaRuntimeZoneContactRow" "Zone-contacts feed must carry typed contact rows."
|
||||
Assert-Contains $zoneRenderDocs "AetheriaRuntimeZoneTargetRow" "Zone-contacts feed must carry typed target rows."
|
||||
Assert-Contains $zoneRenderDocs "public double Distance" "Zone-contacts rows must carry target/contact distance."
|
||||
Assert-Contains $zoneRenderDocs "public double TargetPositionX" "Zone-contacts rows must carry target position."
|
||||
Assert-Contains $zoneRenderDocs "ZoneRenderRadius" "Zone-render feed must carry render radius as an explicit fact."
|
||||
Assert-Contains $zoneRenderDocs "AdjacentZones" "Zone-render feed must carry adjacent-zone summaries for renderer wormholes."
|
||||
Assert-Contains $zoneRenderDocs "WormholeExits" "Zone-render feed must carry resolved wormhole exits as explicit render facts."
|
||||
Assert-Contains $zoneRenderDocs "BodyPoses" "Zone-render feed must carry body poses as explicit render facts."
|
||||
Assert-Contains $zoneRenderDocs "AsteroidBeltPoses" "Zone-render feed must carry asteroid belt poses as explicit render facts."
|
||||
Assert-Contains $zoneRenderDocs "AetheriaRuntimeZoneRenderAsteroidInstancePose" "Zone-render feed must carry typed asteroid instance poses."
|
||||
Assert-Contains $zoneRenderDocs "DroppedPickups" "Zone-render feed must carry dropped pickups as explicit render facts."
|
||||
Assert-Contains $zoneRenderDocs "EntityFacades" "Zone-render feed must carry typed entity facade payloads for Unity shell restoration."
|
||||
Assert-Contains $zoneRenderDocs "public IReadOnlyList<AetheriaRuntimeOrbitSnapshotCommit> Orbits" "Zone-render feed must carry typed orbit rows for Unity zone context lowering."
|
||||
Assert-Contains $zoneRenderDocs "public IReadOnlyList<AetheriaRuntimeBodySnapshotCommit> Bodies" "Zone-render feed must carry typed body rows for Unity zone context lowering."
|
||||
Assert-NotContains $zoneRenderDocs "public AetheriaRuntimeRunCheckpointCommit Run" "Zone-render feed must not expose the whole daemon run snapshot."
|
||||
Assert-NotContains $zoneRenderDocs "public AetheriaRuntimeZoneSnapshotCommit Zone" "Zone-render feed must not expose the whole daemon zone snapshot."
|
||||
|
||||
$zoneRenderer = Read-Text "Assets\Scripts\Zone Display\ZoneRenderer.cs"
|
||||
Assert-Contains $zoneRenderer "ApplyZoneRender" "ZoneRenderer must consume the typed zone-render feed."
|
||||
Assert-Contains $zoneRenderer "_daemonCurrentEntityKey" "ZoneRenderer must track current entity as a render-feed fact, not through a run snapshot."
|
||||
Assert-Contains $zoneRenderer "_daemonSimulationTimeSeconds" "ZoneRenderer must track simulation time as a render-feed fact."
|
||||
Assert-Contains $zoneRenderer "_zoneRenderBodyPoses" "ZoneRenderer must consume body poses from the zone-render feed."
|
||||
Assert-Contains $zoneRenderer "_zoneRenderAsteroidBeltPoses" "ZoneRenderer must consume asteroid belt poses from the zone-render feed."
|
||||
Assert-Contains $zoneRenderer "beltPose.InstancePoses" "ZoneRenderer asteroid rendering must consume typed asteroid instance poses from the zone-render feed."
|
||||
Assert-Contains $zoneRenderer "RestoreDroppedPickupsFromZoneRender" "ZoneRenderer dropped-pickup restore must consume the typed zone-render feed."
|
||||
Assert-Contains $zoneRenderer "ZoneContactsAsync()" "ZoneRenderer target distance cache must read the typed zone-contacts feed."
|
||||
Assert-Contains $zoneRenderer "_daemonTargetRowsByEntityIndex" "ZoneRenderer target distance cache must store typed target rows."
|
||||
Assert-Contains $zoneRenderer "_daemonContactRows" "ZoneRenderer compass and visibility state must cache typed contact rows."
|
||||
Assert-Contains $zoneRenderer "ObjectsViewportAsync(ToViewportBounds(viewport))" "ZoneRenderer presentation loading must use the typed objects viewport facade."
|
||||
Assert-Contains $zoneRenderer "GravityViewportAsync(viewport)" "ZoneRenderer body view loading must use the typed gravity viewport facade."
|
||||
Assert-Contains $zoneRenderDocs "public AetheriaRuntimeBodySnapshotCommit Body" "Gravity/body viewport rows must carry the typed body payload needed for Unity presentation lowering."
|
||||
Assert-Contains $zoneRenderDocs "public double TerrainRadius" "Gravity viewport rows must carry terrain radius for local renderer height sampling."
|
||||
Assert-Contains $zoneRenderDocs "public double TerrainDepth" "Gravity viewport rows must carry terrain depth for local renderer height sampling."
|
||||
Assert-Contains $zoneRenderDocs "public double TerrainDepthExponent" "Gravity viewport rows must carry terrain exponent for local renderer height sampling."
|
||||
Assert-Contains $zoneRenderDocs "public double TerrainWaveFrequency" "Gravity viewport rows must carry wave frequency for local renderer height sampling."
|
||||
Assert-Contains $zoneRenderer "_daemonGravityViewport" "ZoneRenderer terrain sampling must use the cached typed gravity viewport."
|
||||
Assert-Contains $zoneRenderer "ZoneRenderRadius" "ZoneRenderer must consume render radius from the zone-render feed."
|
||||
Assert-NotContains $zoneRenderer "_daemonRunSnapshot" "ZoneRenderer must not retain the whole daemon run snapshot."
|
||||
Assert-NotContains $zoneRenderer "_daemonZoneSnapshot" "ZoneRenderer must not retain the whole daemon zone snapshot."
|
||||
Assert-NotContains $zoneRenderer "ResolveZoneRenderRadius(" "ZoneRenderer must not derive render radius from the zone snapshot."
|
||||
Assert-NotContains $zoneRenderer "QueryBodyPoses(" "ZoneRenderer must not recompute body poses from the zone snapshot."
|
||||
Assert-NotContains $zoneRenderer "QueryBodyViews(" "ZoneRenderer must not compute visible body views from raw zone snapshots."
|
||||
Assert-NotContains $zoneRenderer "QueryAsteroidBeltPoses(" "ZoneRenderer must not recompute asteroid belt poses from the zone snapshot."
|
||||
Assert-NotContains $zoneRenderer "QueryAsteroidInstancePoses(" "ZoneRenderer must not recompute asteroid instance poses from the zone snapshot."
|
||||
Assert-NotContains $zoneRenderer "AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight" "ZoneRenderer must not sample gravity terrain height from raw zone snapshots."
|
||||
Assert-NotContains $zoneRenderer "AetheriaRuntimeDaemonRenderQueries.QueryGravityTerrainBand" "ZoneRenderer must not compute gravity terrain bands from raw zone snapshots."
|
||||
Assert-NotContains $zoneRenderer "TryQueryEntityTarget(" "ZoneRenderer must not query target distances from raw zone snapshots."
|
||||
Assert-NotContains $zoneRenderer "QueryCompassMarkers(" "ZoneRenderer must not recompute compass markers from raw zone snapshots."
|
||||
Assert-NotContains $zoneRenderer "QueryVisibleEntityIndices(" "ZoneRenderer must not recompute visible entity indices from raw zone snapshots."
|
||||
Assert-NotContains $zoneRenderer "QueryPresentationEntityIndices(" "ZoneRenderer must not compute presentation entity loading from raw zone snapshots."
|
||||
|
||||
$actionGameManager = Read-Text "Assets\Scripts\Gameplay\ActionGameManager.cs"
|
||||
$observedFrameApplier = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityObservedFrameApplier.cs"
|
||||
$gameplayLoopShell = Read-Text "Assets\Scripts\Gameplay\AetheriaUnityGameplayLoopShell.cs"
|
||||
Assert-Contains $actionGameManager "ObservedFrameApplier.ApplyLatestZoneRender()" "ActionGameManager must delegate typed zone-render application to the observed frame applier."
|
||||
Assert-Contains $actionGameManager "private AetheriaUnityGameplayLoopShell GameplayLoopShell =>" "ActionGameManager should delegate per-frame Unity presentation/input ticking to the gameplay loop shell."
|
||||
Assert-Contains $actionGameManager "GameplayLoopShell.Tick(Time.deltaTime, Time.time)" "ActionGameManager Update should only forward time into the gameplay loop shell."
|
||||
Assert-Contains $actionGameManager "GameplayLoopShell.LateTick()" "ActionGameManager LateUpdate should only forward into the gameplay loop shell."
|
||||
Assert-Contains $gameplayLoopShell "public sealed class AetheriaUnityGameplayLoopShell" "Unity gameplay loop presentation ticking should live outside ActionGameManager."
|
||||
Assert-Contains $gameplayLoopShell "ApplyLatestZoneRender?.Invoke();" "Gameplay loop shell should apply the latest typed zone-render feed."
|
||||
Assert-Contains $gameplayLoopShell "CurrentEntityPresentation?.Tick(deltaTime);" "Gameplay loop shell should tick current-entity presentation."
|
||||
Assert-Contains $gameplayLoopShell "PilotFrameAdapter?.Tick(ResolveCurrentEntity?.Invoke(), deltaTime, time);" "Gameplay loop shell should tick pilot frame presentation/input."
|
||||
Assert-Contains $gameplayLoopShell "TargetPresentation?.UpdateTargetIndicators(" "Gameplay loop shell should tick target indicators."
|
||||
Assert-NotContains $actionGameManager "_currentEntityPresentation.Tick(Time.deltaTime);" "ActionGameManager must not tick current-entity presentation directly."
|
||||
Assert-NotContains $actionGameManager "PilotFrameAdapter.Tick(CurrentEntity, Time.deltaTime, Time.time)" "ActionGameManager must not tick pilot frame presentation/input directly."
|
||||
Assert-NotContains $actionGameManager "_targetPresentation.UpdateTargetIndicators(" "ActionGameManager must not tick target indicators directly."
|
||||
Assert-NotContains $actionGameManager "_paused" "ActionGameManager must not own pause state; the loop shell owns it."
|
||||
Assert-NotContains $actionGameManager "_time" "ActionGameManager must not keep unused local simulation time."
|
||||
Assert-Contains $observedFrameApplier "TryRestoreEntityGraphFromZoneRender" "Observed frame applier must restore Unity shell state from the typed zone-render feed."
|
||||
Assert-Contains $observedFrameApplier "ApplyLatestZoneRender" "Observed frame applier must name the handoff as zone-render application."
|
||||
Assert-Contains $observedFrameApplier "ZoneRenderAsync()" "Observed frame applier must use the shared zone-render facade at the frame handoff."
|
||||
Assert-Contains $observedFrameApplier "render.EntityFacades" "Observed frame applier entity restoration must consume the typed zone-render entity facade payload."
|
||||
Assert-NotContains $observedFrameApplier "Func<Galaxy>" "Observed frame applier must not depend on the whole observed galaxy facade."
|
||||
Assert-Contains $observedFrameApplier "_resolveObservedZone" "Observed frame applier should name the narrow target-zone resolver explicitly."
|
||||
Assert-NotContains $observedFrameApplier "resolveObservedGalaxy" "Observed frame applier must resolve only the target zone needed for Unity shell adaptation."
|
||||
Assert-NotContains $observedFrameApplier "render?.Zone" "Observed frame applier must not reach through zone-render into a whole zone snapshot."
|
||||
Assert-NotContains $observedFrameApplier "ApplyLatestAuthoritativeDaemonFrame" "Observed frame applier must not describe zone-render application as whole daemon frame application."
|
||||
Assert-NotContains $observedFrameApplier "Authoritative daemon frame" "Observed frame applier diagnostics must describe the typed zone-render feed, not a whole daemon frame."
|
||||
Assert-NotContains $gameplayLoopShell "ApplyLatestAuthoritativeDaemonFrame" "Gameplay loop shell must not describe zone-render application as whole daemon frame application."
|
||||
Assert-NotContains $actionGameManager "TryRestoreEntityGraphFromDaemonRun" "ActionGameManager must not restore Unity shell state directly from raw daemon runs."
|
||||
Assert-NotMatches $actionGameManager "render\.Run(?!Id\b)" "ActionGameManager must not reach through zone-render into a whole run snapshot."
|
||||
Assert-NotContains $actionGameManager "StampDaemonZoneSimulationTime" "ActionGameManager must not mutate zone snapshots to carry render timing."
|
||||
|
||||
Write-Host "Stage 7D Unity parity verifier passed."
|
||||
|
|
@ -45,14 +45,54 @@ public class ActionBarSlot : MonoBehaviour
|
|||
public abstract class ActionBarBinding
|
||||
{
|
||||
public Entity Entity { get; }
|
||||
protected AetheriaClient Client { get; }
|
||||
protected ActionBarSlot Slot { get; }
|
||||
protected GameSettings Settings { get; }
|
||||
private AetheriaRuntimeCatalogSnapshot _catalog;
|
||||
public abstract void Activate();
|
||||
public abstract void Deactivate();
|
||||
public abstract void Update();
|
||||
public ActionBarBinding(Entity entity, ActionBarSlot slot)
|
||||
|
||||
protected ActionBarBinding(Entity entity, ActionBarSlot slot, AetheriaClient client, GameSettings settings)
|
||||
{
|
||||
Entity = entity;
|
||||
Slot = slot;
|
||||
Client = client;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
protected bool TrySubmit(Action<AetheriaControl> submit, string label)
|
||||
{
|
||||
if (Client == null || submit == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
submit(Client.Control);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to send Aetheria daemon action-bar {label} operation; operation not submitted: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected AetheriaRuntimeCatalogItem FindCatalogItem(ItemInstance item)
|
||||
{
|
||||
if (item == null || string.IsNullOrWhiteSpace(item.ItemKey) || Client == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
_catalog ??= Client.OpenRuntimeCatalog();
|
||||
return _catalog?.FindItem(item.ItemKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to read Aetheria runtime catalog for action-bar binding: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +103,9 @@ public class ActionBarConsumableBinding : ActionBarBinding
|
|||
public ActionBarConsumableBinding(
|
||||
Entity entity,
|
||||
ActionBarSlot slot,
|
||||
AetheriaRuntimeCatalogItem target) : base(entity, slot)
|
||||
AetheriaClient client,
|
||||
GameSettings settings,
|
||||
AetheriaRuntimeCatalogItem target) : base(entity, slot, client, settings)
|
||||
{
|
||||
Target = target;
|
||||
Slot.QuantityRemaining.gameObject.SetActive(true);
|
||||
|
|
@ -73,7 +115,9 @@ public class ActionBarConsumableBinding : ActionBarBinding
|
|||
|
||||
public override void Activate()
|
||||
{
|
||||
ActionGameManager.Instance.RequestActionBarConsumable(TargetItemKey);
|
||||
TrySubmit(
|
||||
operations => operations.ActivateConsumable(TargetItemKey),
|
||||
"consumable");
|
||||
}
|
||||
|
||||
public override void Deactivate()
|
||||
|
|
@ -111,7 +155,13 @@ public class ActionBarGearBinding : ActionBarBinding
|
|||
|
||||
public string TargetItemKey => Item?.EquippableItem?.ItemKey ?? "";
|
||||
|
||||
public ActionBarGearBinding(Entity entity, ActionBarSlot slot, EquippedItem item, IActivatedBehavior behavior) : base(entity, slot)
|
||||
public ActionBarGearBinding(
|
||||
Entity entity,
|
||||
ActionBarSlot slot,
|
||||
AetheriaClient client,
|
||||
GameSettings settings,
|
||||
EquippedItem item,
|
||||
IActivatedBehavior behavior) : base(entity, slot, client, settings)
|
||||
{
|
||||
Item = item;
|
||||
Behavior = behavior;
|
||||
|
|
@ -123,7 +173,7 @@ public class ActionBarGearBinding : ActionBarBinding
|
|||
|
||||
private Texture2D ResolveIconTexture()
|
||||
{
|
||||
var typedItem = FindTypedGearItem(Item.EquippableItem);
|
||||
var typedItem = FindCatalogItem(Item.EquippableItem);
|
||||
if (typedItem != null)
|
||||
{
|
||||
var actionBarIcon = LoadActionBarIcon(typedItem.ActionBarIcon);
|
||||
|
|
@ -131,13 +181,13 @@ public class ActionBarGearBinding : ActionBarBinding
|
|||
return actionBarIcon;
|
||||
|
||||
if (Enum.TryParse<WeaponType>(typedItem.WeaponType, out var weaponType))
|
||||
return ActionGameManager.Instance.Settings.GetIcon(weaponType).texture;
|
||||
return Settings.GetIcon(weaponType).texture;
|
||||
|
||||
if (Enum.TryParse<HardpointType>(typedItem.HardpointType, out var hardpointType))
|
||||
return ActionGameManager.Instance.Settings.GetIcon(hardpointType).texture;
|
||||
return Settings.GetIcon(hardpointType).texture;
|
||||
}
|
||||
|
||||
return ActionGameManager.Instance.Settings.GetIcon(HardpointType.Tool).texture;
|
||||
return Settings.GetIcon(HardpointType.Tool).texture;
|
||||
}
|
||||
|
||||
private static Texture2D LoadActionBarIcon(string path)
|
||||
|
|
@ -154,21 +204,20 @@ public class ActionBarGearBinding : ActionBarBinding
|
|||
return Resources.Load<Texture2D>(resourcePath);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCatalogItem FindTypedGearItem(ItemInstance item)
|
||||
{
|
||||
return ActionGameManager.RuntimeCatalog?.FindItem(item?.ItemKey ?? "");
|
||||
}
|
||||
|
||||
public override void Activate()
|
||||
{
|
||||
Active = true;
|
||||
ActionGameManager.Instance.RequestActionBarBehavior(EquipmentIndex, BehaviorIndex, true);
|
||||
TrySubmit(
|
||||
operations => operations.SetBehaviorActive(EquipmentIndex, BehaviorIndex, true),
|
||||
"behavior activation");
|
||||
}
|
||||
|
||||
public override void Deactivate()
|
||||
{
|
||||
Active = false;
|
||||
ActionGameManager.Instance.RequestActionBarBehavior(EquipmentIndex, BehaviorIndex, false);
|
||||
TrySubmit(
|
||||
operations => operations.SetBehaviorActive(EquipmentIndex, BehaviorIndex, false),
|
||||
"behavior activation");
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
|
|
@ -181,7 +230,12 @@ public class ActionBarWeaponGroupBinding : ActionBarBinding
|
|||
{
|
||||
public int Group;
|
||||
|
||||
public ActionBarWeaponGroupBinding(Entity entity, ActionBarSlot slot, int group) : base(entity, slot)
|
||||
public ActionBarWeaponGroupBinding(
|
||||
Entity entity,
|
||||
ActionBarSlot slot,
|
||||
AetheriaClient client,
|
||||
GameSettings settings,
|
||||
int group) : base(entity, slot, client, settings)
|
||||
{
|
||||
Group = group;
|
||||
slot.Label.gameObject.SetActive(true);
|
||||
|
|
@ -191,12 +245,16 @@ public class ActionBarWeaponGroupBinding : ActionBarBinding
|
|||
|
||||
public override void Activate()
|
||||
{
|
||||
ActionGameManager.Instance.RequestActionBarWeaponGroup(Group, true);
|
||||
TrySubmit(
|
||||
operations => operations.SetWeaponGroupActive(Group, true),
|
||||
"weapon-group");
|
||||
}
|
||||
|
||||
public override void Deactivate()
|
||||
{
|
||||
ActionGameManager.Instance.RequestActionBarWeaponGroup(Group, false);
|
||||
TrySubmit(
|
||||
operations => operations.SetWeaponGroupActive(Group, false),
|
||||
"weapon-group");
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -18,10 +18,8 @@ public sealed class AetheriaDaemonObserver : MonoBehaviour
|
|||
|
||||
private readonly AetheriaRuntimeDaemonObservationCursor _cursor = new AetheriaRuntimeDaemonObservationCursor();
|
||||
private float _nextPollTime;
|
||||
private AetheriaRuntimeDaemonOperationClient _operationClient;
|
||||
private AetheriaRuntimeDaemonOperationsClient _operations;
|
||||
private AetheriaRuntimeVerseClient _verseClient;
|
||||
private string _verseClientStatePath;
|
||||
private AetheriaClient _client;
|
||||
private string _clientStatePath;
|
||||
private AetheriaDaemonSoaMemoryMap _soaMemoryMap;
|
||||
private AetheriaDaemonRenderNativeView _renderNativeView;
|
||||
|
||||
|
|
@ -36,34 +34,11 @@ public sealed class AetheriaDaemonObserver : MonoBehaviour
|
|||
public AetheriaDaemonSoaMemoryMap LastSoaMemoryMap => _soaMemoryMap;
|
||||
public AetheriaDaemonRenderNativeView LastRenderNativeView => _renderNativeView;
|
||||
public bool HasRenderNativeView => _renderNativeView.IsCreated;
|
||||
public AetheriaRuntimeDaemonOperationsClient Operations =>
|
||||
_operations ??= new AetheriaRuntimeDaemonOperationsClient(SendOperation);
|
||||
public AetheriaClient Client => ResolveClient();
|
||||
public AetheriaControl Control => Client.Control;
|
||||
|
||||
public event Action<AetheriaRuntimeObservedDaemonState, AetheriaRuntimeDaemonObservationResult> ObservedDaemonStateChanged;
|
||||
|
||||
internal AetheriaRuntimeDaemonCommandEnvelope SendOperation(
|
||||
Func<AetheriaRuntimeDaemonOperationClient, AetheriaRuntimeObservedDaemonState, AetheriaRuntimeDaemonCommandEnvelope> submit)
|
||||
{
|
||||
if (submit == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(submit));
|
||||
}
|
||||
|
||||
_operationClient ??= new AetheriaRuntimeDaemonOperationClient(
|
||||
ActionGameManager.RuntimeStateFilePath,
|
||||
clientId,
|
||||
LastObservedState?.Frame.SessionId ?? "local");
|
||||
|
||||
var envelope = submit(_operationClient, LastObservedState);
|
||||
|
||||
if (logChanges)
|
||||
{
|
||||
Debug.Log($"Submitted Aetheria daemon operation {envelope.Kind}: {envelope.CommandId}");
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!pollInUpdate || Time.unscaledTime < _nextPollTime)
|
||||
|
|
@ -117,38 +92,42 @@ public sealed class AetheriaDaemonObserver : MonoBehaviour
|
|||
|
||||
private void OnDisable()
|
||||
{
|
||||
DisposeVerseClient();
|
||||
DisposeClient();
|
||||
DisposeSoaMemoryMap();
|
||||
}
|
||||
|
||||
private AetheriaRuntimeObservedDaemonState ReadObservedDaemonState()
|
||||
{
|
||||
var client = ResolveVerseClient();
|
||||
return client.GetObservedDaemonStateAsync().GetAwaiter().GetResult();
|
||||
return ResolveClient().ObserveAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private AetheriaRuntimeVerseClient ResolveVerseClient()
|
||||
private AetheriaClient ResolveClient()
|
||||
{
|
||||
var statePath = ActionGameManager.RuntimeStateFilePath;
|
||||
if (_verseClient != null && string.Equals(_verseClientStatePath, statePath, StringComparison.Ordinal))
|
||||
var statePath = AetheriaUnityRuntimePaths.RuntimeStateFilePath;
|
||||
if (_client != null && string.Equals(_clientStatePath, statePath, StringComparison.Ordinal))
|
||||
{
|
||||
return _verseClient;
|
||||
return _client;
|
||||
}
|
||||
|
||||
DisposeVerseClient();
|
||||
_verseClient = AetheriaRuntimeVerseClient
|
||||
.OpenAsync(statePath, clientId, startServer: false, pullOnOpen: true)
|
||||
DisposeClient();
|
||||
_client = AetheriaClient
|
||||
.OpenAsync(
|
||||
statePath,
|
||||
clientId,
|
||||
LastObservedState?.Frame.SessionId ?? "local",
|
||||
startServer: false,
|
||||
pullOnOpen: true)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
_verseClientStatePath = statePath;
|
||||
return _verseClient;
|
||||
_clientStatePath = statePath;
|
||||
return _client;
|
||||
}
|
||||
|
||||
private void DisposeVerseClient()
|
||||
private void DisposeClient()
|
||||
{
|
||||
_verseClient?.Dispose();
|
||||
_verseClient = null;
|
||||
_verseClientStatePath = null;
|
||||
_client?.Dispose();
|
||||
_client = null;
|
||||
_clientStatePath = null;
|
||||
}
|
||||
|
||||
private void RemapSoaView(AetheriaRuntimeObservedDaemonState observed)
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c70095f8b49f42446aa3b0b9cb76356f
|
||||
|
|
@ -140,9 +140,7 @@ public sealed class AetheriaDaemonRenderBuffer : MonoBehaviour
|
|||
using var writtenCount = new NativeArray<int>(1, Allocator.TempJob);
|
||||
var job = new AetheriaDaemonRenderGroupMatrixJob
|
||||
{
|
||||
PositionX = view.PositionX,
|
||||
PositionY = view.PositionY,
|
||||
PositionZ = view.PositionZ,
|
||||
Position = view.Position,
|
||||
RotationRadians = view.RotationRadians,
|
||||
PhysicsBodyRadius = view.PhysicsBodyRadius,
|
||||
RenderScale = view.RenderScale,
|
||||
|
|
|
|||
|
|
@ -104,9 +104,7 @@ public sealed class AetheriaDaemonRenderGroupBatch : IDisposable
|
|||
using var writtenCount = new NativeArray<int>(1, Allocator.TempJob);
|
||||
var job = new AetheriaDaemonRenderGroupMatrixJob
|
||||
{
|
||||
PositionX = view.PositionX,
|
||||
PositionY = view.PositionY,
|
||||
PositionZ = view.PositionZ,
|
||||
Position = view.Position,
|
||||
RotationRadians = view.RotationRadians,
|
||||
PhysicsBodyRadius = view.PhysicsBodyRadius,
|
||||
RenderScale = view.RenderScale,
|
||||
|
|
|
|||
|
|
@ -7,13 +7,7 @@ using Unity.Mathematics;
|
|||
public struct AetheriaDaemonRenderGroupMatrixJob : IJob
|
||||
{
|
||||
[ReadOnly]
|
||||
public NativeArray<float> PositionX;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<float> PositionY;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<float> PositionZ;
|
||||
public NativeArray<float3> Position;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<float> RotationRadians;
|
||||
|
|
@ -51,7 +45,7 @@ public struct AetheriaDaemonRenderGroupMatrixJob : IJob
|
|||
public void Execute()
|
||||
{
|
||||
var written = 0;
|
||||
for (var index = 0; index < PositionX.Length; index++)
|
||||
for (var index = 0; index < Position.Length; index++)
|
||||
{
|
||||
if (HasRenderGroupFilter && RenderGroupId[index] != TargetRenderGroupId)
|
||||
{
|
||||
|
|
@ -73,7 +67,7 @@ public struct AetheriaDaemonRenderGroupMatrixJob : IJob
|
|||
break;
|
||||
}
|
||||
|
||||
var position = new float3(PositionX[index], PositionY[index], PositionZ[index]);
|
||||
var position = Position[index];
|
||||
var rotation = HasRotation
|
||||
? quaternion.RotateY(RotationRadians[index])
|
||||
: quaternion.identity;
|
||||
|
|
|
|||
|
|
@ -7,13 +7,7 @@ using Unity.Mathematics;
|
|||
public struct AetheriaDaemonRenderMatrixJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly]
|
||||
public NativeArray<float> PositionX;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<float> PositionY;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<float> PositionZ;
|
||||
public NativeArray<float3> Position;
|
||||
|
||||
[ReadOnly]
|
||||
public NativeArray<float> RotationRadians;
|
||||
|
|
@ -34,7 +28,7 @@ public struct AetheriaDaemonRenderMatrixJob : IJobParallelFor
|
|||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var position = new float3(PositionX[index], PositionY[index], PositionZ[index]);
|
||||
var position = Position[index];
|
||||
var rotation = HasRotation
|
||||
? quaternion.RotateY(RotationRadians[index])
|
||||
: quaternion.identity;
|
||||
|
|
|
|||
|
|
@ -9,13 +9,9 @@ public struct AetheriaDaemonRenderNativeView
|
|||
public long Generation;
|
||||
public int Count;
|
||||
public NativeArray<int> EntityIndex;
|
||||
public NativeArray<float> PositionX;
|
||||
public NativeArray<float> PositionY;
|
||||
public NativeArray<float> PositionZ;
|
||||
public NativeArray<float3> Position;
|
||||
public NativeArray<float> RotationRadians;
|
||||
public NativeArray<float> VelocityX;
|
||||
public NativeArray<float> VelocityY;
|
||||
public NativeArray<float> VelocityZ;
|
||||
public NativeArray<float3> Velocity;
|
||||
public NativeArray<float> PhysicsBodyRadius;
|
||||
public NativeArray<float> PhysicsBodyMass;
|
||||
public NativeArray<float> PhysicsBodyInverseMass;
|
||||
|
|
@ -26,13 +22,11 @@ public struct AetheriaDaemonRenderNativeView
|
|||
|
||||
public bool IsCreated =>
|
||||
Count > 0 &&
|
||||
PositionX.IsCreated &&
|
||||
PositionY.IsCreated &&
|
||||
PositionZ.IsCreated;
|
||||
Position.IsCreated;
|
||||
|
||||
public bool HasRotation => RotationRadians.IsCreated;
|
||||
public bool HasEntityIndex => EntityIndex.IsCreated;
|
||||
public bool HasVelocity => VelocityX.IsCreated && VelocityY.IsCreated && VelocityZ.IsCreated;
|
||||
public bool HasVelocity => Velocity.IsCreated;
|
||||
public bool HasPhysicsRadius => PhysicsBodyRadius.IsCreated;
|
||||
public bool HasPhysicsMass => PhysicsBodyMass.IsCreated;
|
||||
public bool HasPhysicsInverseMass => PhysicsBodyInverseMass.IsCreated;
|
||||
|
|
@ -60,9 +54,7 @@ public struct AetheriaDaemonRenderNativeView
|
|||
|
||||
var job = new AetheriaDaemonRenderMatrixJob
|
||||
{
|
||||
PositionX = PositionX,
|
||||
PositionY = PositionY,
|
||||
PositionZ = PositionZ,
|
||||
Position = Position,
|
||||
RotationRadians = RotationRadians,
|
||||
PhysicsBodyRadius = PhysicsBodyRadius,
|
||||
RenderScale = RenderScale,
|
||||
|
|
@ -89,33 +81,18 @@ public struct AetheriaDaemonRenderNativeView
|
|||
|
||||
if (!map.TryCreateFirstNativeArrayOfKind(
|
||||
index,
|
||||
AetheriaRuntimeDaemonSoaColumnKinds.PositionX,
|
||||
out view.PositionX) ||
|
||||
!map.TryCreateFirstNativeArrayOfKind(
|
||||
index,
|
||||
AetheriaRuntimeDaemonSoaColumnKinds.PositionY,
|
||||
out view.PositionY) ||
|
||||
!map.TryCreateFirstNativeArrayOfKind(
|
||||
index,
|
||||
AetheriaRuntimeDaemonSoaColumnKinds.PositionZ,
|
||||
out view.PositionZ))
|
||||
AetheriaRuntimeDaemonSoaColumnKinds.Position,
|
||||
out view.Position))
|
||||
{
|
||||
view = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
view.Count = view.PositionX.Length;
|
||||
if (view.PositionY.Length != view.Count || view.PositionZ.Length != view.Count)
|
||||
{
|
||||
view = default;
|
||||
return false;
|
||||
}
|
||||
view.Count = view.Position.Length;
|
||||
|
||||
TryAssignOptionalIntColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.EntityIndex, view.Count, out view.EntityIndex);
|
||||
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.RotationRadians, view.Count, out view.RotationRadians);
|
||||
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.VelocityX, view.Count, out view.VelocityX);
|
||||
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.VelocityY, view.Count, out view.VelocityY);
|
||||
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.VelocityZ, view.Count, out view.VelocityZ);
|
||||
TryAssignOptionalFloat3Column(index, map, AetheriaRuntimeDaemonSoaColumnKinds.Velocity, view.Count, out view.Velocity);
|
||||
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyRadius, view.Count, out view.PhysicsBodyRadius);
|
||||
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyMass, view.Count, out view.PhysicsBodyMass);
|
||||
TryAssignOptionalColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyInverseMass, view.Count, out view.PhysicsBodyInverseMass);
|
||||
|
|
@ -124,19 +101,39 @@ public struct AetheriaDaemonRenderNativeView
|
|||
TryAssignOptionalIntColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.RenderLod, view.Count, out view.RenderLod);
|
||||
TryAssignOptionalUIntColumn(index, map, AetheriaRuntimeDaemonSoaColumnKinds.RenderGroupId, view.Count, out view.RenderGroupId);
|
||||
|
||||
if ((view.VelocityX.IsCreated || view.VelocityY.IsCreated || view.VelocityZ.IsCreated) &&
|
||||
!view.HasVelocity)
|
||||
{
|
||||
view.VelocityX = default;
|
||||
view.VelocityY = default;
|
||||
view.VelocityZ = default;
|
||||
}
|
||||
|
||||
view.FrameId = index.View.FrameId;
|
||||
view.Generation = index.View.Generation;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryAssignOptionalFloat3Column(
|
||||
AetheriaRuntimeDaemonSoaViewIndex index,
|
||||
AetheriaDaemonSoaMemoryMap map,
|
||||
string kind,
|
||||
int expectedCount,
|
||||
out NativeArray<float3> array)
|
||||
{
|
||||
array = default;
|
||||
if (!index.TryGetFirstColumnOfKind(kind, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!map.TryCreateFirstNativeArrayOfKind(index, kind, out array))
|
||||
{
|
||||
array = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (array.Length == expectedCount)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
array = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryAssignOptionalColumn(
|
||||
AetheriaRuntimeDaemonSoaViewIndex index,
|
||||
AetheriaDaemonSoaMemoryMap map,
|
||||
|
|
|
|||
13
Assets/Scripts/Gameplay/AetheriaUnityActionBarBinding.cs
Normal file
13
Assets/Scripts/Gameplay/AetheriaUnityActionBarBinding.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
public sealed class AetheriaUnityActionBarBinding
|
||||
{
|
||||
public string ControlPath { get; set; } = "";
|
||||
public string Kind { get; set; } = "";
|
||||
public string ItemKey { get; set; } = "";
|
||||
public int EquipmentIndex { get; set; } = -1;
|
||||
public int BehaviorIndex { get; set; } = -1;
|
||||
public int WeaponGroup { get; set; } = -1;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: af36b057bd1c2ba46adad018b02fd568
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public sealed class AetheriaUnityActionBarBindingAdapter
|
||||
{
|
||||
private readonly AetheriaUnityActionBarPresentation _presentation;
|
||||
|
||||
public AetheriaUnityActionBarBindingAdapter(
|
||||
AetheriaUnityActionBarPresentation presentation)
|
||||
{
|
||||
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
||||
}
|
||||
|
||||
public void ApplyBindings(IReadOnlyList<AetheriaUnityActionBarBinding> bindings)
|
||||
{
|
||||
_presentation.ApplyLocalBindings();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c70aab770e351924eb213421f9096e22
|
||||
275
Assets/Scripts/Gameplay/AetheriaUnityActionBarPresentation.cs
Normal file
275
Assets/Scripts/Gameplay/AetheriaUnityActionBarPresentation.cs
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class AetheriaUnityActionBarPresentation
|
||||
{
|
||||
private IReadOnlyList<ActionBarSlot> _slots = Array.Empty<ActionBarSlot>();
|
||||
private AetheriaRuntimeCatalogSnapshot _catalog;
|
||||
private GameSettings _settings;
|
||||
private Func<Entity> _resolveEntity = () => null;
|
||||
private Func<AetheriaClient> _resolveClient = () => null;
|
||||
private IReadOnlyList<AetheriaUnityActionBarBinding> _localBindings =
|
||||
Array.Empty<AetheriaUnityActionBarBinding>();
|
||||
|
||||
public void Bind(
|
||||
IReadOnlyList<ActionBarSlot> slots,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
GameSettings settings,
|
||||
Func<Entity> resolveEntity,
|
||||
Func<AetheriaClient> resolveClient)
|
||||
{
|
||||
_slots = slots ?? Array.Empty<ActionBarSlot>();
|
||||
_catalog = catalog;
|
||||
_settings = settings;
|
||||
_resolveEntity = resolveEntity ?? (() => null);
|
||||
_resolveClient = resolveClient ?? (() => null);
|
||||
}
|
||||
|
||||
public int SlotCount => _slots?.Count ?? 0;
|
||||
|
||||
public string SlotLabel(int slotIndex)
|
||||
{
|
||||
var slot = ResolveSlot(slotIndex);
|
||||
if (slot == null)
|
||||
return "";
|
||||
|
||||
var controlPath = slot.ControlPath ?? "";
|
||||
if (string.IsNullOrWhiteSpace(controlPath))
|
||||
return "Action Bar";
|
||||
|
||||
var slashIndex = controlPath.LastIndexOf('/');
|
||||
return slashIndex >= 0 && slashIndex < controlPath.Length - 1
|
||||
? controlPath.Substring(slashIndex + 1)
|
||||
: controlPath;
|
||||
}
|
||||
|
||||
public string BindingLabel(int slotIndex)
|
||||
{
|
||||
var slot = ResolveSlot(slotIndex);
|
||||
return slot?.Binding switch
|
||||
{
|
||||
ActionBarWeaponGroupBinding weaponGroup => $"G{weaponGroup.Group + 1}",
|
||||
ActionBarConsumableBinding consumable => consumable.Target?.Name ?? "Consumable",
|
||||
ActionBarGearBinding gear => _catalog?.FindItem(gear.Item?.EquippableItem?.ItemKey ?? "")?.Name ?? "Gear",
|
||||
_ => "Empty"
|
||||
};
|
||||
}
|
||||
|
||||
public bool TryResolveControlPath(int slotIndex, out string controlPath)
|
||||
{
|
||||
controlPath = ResolveSlot(slotIndex)?.ControlPath ?? "";
|
||||
return !string.IsNullOrWhiteSpace(controlPath);
|
||||
}
|
||||
|
||||
public void RequestBinding(ActionBarSlot slot, DragObject dragAction)
|
||||
{
|
||||
var entity = _resolveEntity();
|
||||
if (slot == null || dragAction == null || entity == null)
|
||||
return;
|
||||
|
||||
var binding = CreateBindingCommit(slot, entity, dragAction);
|
||||
if (binding == null ||
|
||||
string.IsNullOrWhiteSpace(binding.ControlPath) ||
|
||||
string.IsNullOrWhiteSpace(binding.Kind))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetLocalBinding(binding);
|
||||
ApplyLocalBindings();
|
||||
}
|
||||
|
||||
public bool RequestWeaponGroupBinding(int slotIndex, int groupIndex)
|
||||
{
|
||||
if (groupIndex < 0 || !TryResolveControlPath(slotIndex, out var controlPath))
|
||||
return false;
|
||||
|
||||
SetLocalBinding(new AetheriaUnityActionBarBinding
|
||||
{
|
||||
ControlPath = controlPath,
|
||||
Kind = "weapon_group",
|
||||
ItemKey = "",
|
||||
EquipmentIndex = -1,
|
||||
BehaviorIndex = -1,
|
||||
WeaponGroup = groupIndex
|
||||
});
|
||||
ApplyLocalBindings();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ClearBinding(int slotIndex)
|
||||
{
|
||||
if (!TryResolveControlPath(slotIndex, out var controlPath))
|
||||
return false;
|
||||
|
||||
_localBindings = (_localBindings ?? Array.Empty<AetheriaUnityActionBarBinding>())
|
||||
.Where(binding => !string.Equals(binding?.ControlPath ?? "", controlPath, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
ApplyLocalBindings();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ApplyLocalBindings()
|
||||
{
|
||||
ApplyBindings(_resolveEntity(), _localBindings);
|
||||
}
|
||||
|
||||
public void ApplyBindings(
|
||||
Entity entity,
|
||||
IReadOnlyList<AetheriaUnityActionBarBinding> bindings)
|
||||
{
|
||||
if (_slots == null || _slots.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var slot in _slots)
|
||||
slot.Binding = null;
|
||||
|
||||
if (entity == null || bindings == null || bindings.Count == 0)
|
||||
return;
|
||||
|
||||
var slotsByControlPath = _slots
|
||||
.Where(slot => !string.IsNullOrWhiteSpace(slot?.ControlPath))
|
||||
.GroupBy(slot => slot.ControlPath, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
|
||||
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
if (binding == null ||
|
||||
string.IsNullOrWhiteSpace(binding.ControlPath) ||
|
||||
!slotsByControlPath.TryGetValue(binding.ControlPath, out var slot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var slotBinding = CreateBinding(slot, entity, binding);
|
||||
if (slotBinding != null)
|
||||
slot.Binding = slotBinding;
|
||||
}
|
||||
}
|
||||
|
||||
public ActionBarBinding CreateBinding(ActionBarSlot slot, Entity entity, DragObject dragAction)
|
||||
{
|
||||
var binding = CreateBindingCommit(slot, entity, dragAction);
|
||||
return binding == null ? null : CreateBinding(slot, entity, binding);
|
||||
}
|
||||
|
||||
private AetheriaUnityActionBarBinding CreateBindingCommit(
|
||||
ActionBarSlot slot,
|
||||
Entity entity,
|
||||
DragObject dragAction)
|
||||
{
|
||||
if (slot == null || entity == null || dragAction == null)
|
||||
return null;
|
||||
|
||||
switch (dragAction)
|
||||
{
|
||||
case EquippedItemDragObject equippedItemDragAction:
|
||||
var equippedItem = equippedItemDragAction.EquippedItem;
|
||||
var trigger = equippedItem?.GetBehavior<IActivatedBehavior>();
|
||||
var equipmentIndex = equippedItem == null ? -1 : entity.Equipment.IndexOf(equippedItem);
|
||||
var behaviorIndex = equippedItem?.Behaviors == null ? -1 : Array.IndexOf(equippedItem.Behaviors, trigger);
|
||||
return trigger == null || equipmentIndex < 0 || behaviorIndex < 0
|
||||
? null
|
||||
: new AetheriaUnityActionBarBinding
|
||||
{
|
||||
ControlPath = slot.ControlPath ?? "",
|
||||
Kind = "gear",
|
||||
ItemKey = equippedItem.EquippableItem?.ItemKey ?? "",
|
||||
EquipmentIndex = equipmentIndex,
|
||||
BehaviorIndex = behaviorIndex
|
||||
};
|
||||
case ItemInstanceDragObject itemInstanceDragAction:
|
||||
var consumable = FindConsumable(itemInstanceDragAction.Item);
|
||||
return consumable == null
|
||||
? null
|
||||
: new AetheriaUnityActionBarBinding
|
||||
{
|
||||
ControlPath = slot.ControlPath ?? "",
|
||||
Kind = "consumable",
|
||||
ItemKey = consumable.ItemKey ?? ""
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLocalBinding(AetheriaUnityActionBarBinding binding)
|
||||
{
|
||||
if (binding == null || string.IsNullOrWhiteSpace(binding.ControlPath))
|
||||
return;
|
||||
|
||||
_localBindings = (_localBindings ?? Array.Empty<AetheriaUnityActionBarBinding>())
|
||||
.Where(existing => !string.Equals(existing?.ControlPath ?? "", binding.ControlPath, StringComparison.Ordinal))
|
||||
.Concat(new[] { binding })
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private ActionBarBinding CreateBinding(
|
||||
ActionBarSlot slot,
|
||||
Entity entity,
|
||||
AetheriaUnityActionBarBinding binding)
|
||||
{
|
||||
if (slot == null || entity == null || binding == null)
|
||||
return null;
|
||||
|
||||
var client = _resolveClient();
|
||||
|
||||
switch (binding.Kind)
|
||||
{
|
||||
case "consumable":
|
||||
var consumable = _catalog?.FindItem(binding.ItemKey ?? "");
|
||||
return consumable != null &&
|
||||
string.Equals(consumable.Category, AetheriaRuntimeItemCategories.Consumable, StringComparison.Ordinal)
|
||||
? new ActionBarConsumableBinding(entity, slot, client, _settings, consumable)
|
||||
: null;
|
||||
case "gear":
|
||||
if (binding.EquipmentIndex < 0 || binding.EquipmentIndex >= entity.Equipment.Count)
|
||||
return null;
|
||||
|
||||
var equippedItem = entity.Equipment[binding.EquipmentIndex];
|
||||
var behaviors = equippedItem?.Behaviors;
|
||||
if (equippedItem == null ||
|
||||
!string.Equals(equippedItem.EquippableItem?.ItemKey ?? "", binding.ItemKey ?? "", StringComparison.Ordinal) ||
|
||||
behaviors == null ||
|
||||
binding.BehaviorIndex < 0 ||
|
||||
binding.BehaviorIndex >= behaviors.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(behaviors[binding.BehaviorIndex] is IActivatedBehavior activatedBehavior))
|
||||
return null;
|
||||
|
||||
return new ActionBarGearBinding(entity, slot, client, _settings, equippedItem, activatedBehavior);
|
||||
case "weapon_group":
|
||||
return entity.WeaponGroups != null &&
|
||||
binding.WeaponGroup >= 0 &&
|
||||
binding.WeaponGroup < entity.WeaponGroups.Length
|
||||
? new ActionBarWeaponGroupBinding(entity, slot, client, _settings, binding.WeaponGroup)
|
||||
: null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AetheriaRuntimeCatalogItem FindConsumable(ItemInstance item)
|
||||
{
|
||||
var typedItem = _catalog?.FindItem(item?.ItemKey ?? "");
|
||||
return typedItem != null &&
|
||||
string.Equals(typedItem.Category, AetheriaRuntimeItemCategories.Consumable, StringComparison.Ordinal)
|
||||
? typedItem
|
||||
: null;
|
||||
}
|
||||
|
||||
private ActionBarSlot ResolveSlot(int slotIndex)
|
||||
{
|
||||
return _slots != null &&
|
||||
slotIndex >= 0 &&
|
||||
slotIndex < _slots.Count
|
||||
? _slots[slotIndex]
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0f239f607245e8143805587f4c34fbee
|
||||
38
Assets/Scripts/Gameplay/AetheriaUnityCockpitHudShell.cs
Normal file
38
Assets/Scripts/Gameplay/AetheriaUnityCockpitHudShell.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
public sealed class AetheriaUnityCockpitHudShell
|
||||
{
|
||||
public InventoryPanel ShipPanel { get; set; }
|
||||
public InventoryPanel TargetShipPanel { get; set; }
|
||||
public SchematicDisplay SchematicDisplay { get; set; }
|
||||
public SchematicDisplay TargetSchematicDisplay { get; set; }
|
||||
public PlaceUIElementWorldspace TargetIndicator { get; set; }
|
||||
|
||||
public void SetRenderSettings(AetheriaRuntimeDaemonRenderSettings renderSettings)
|
||||
{
|
||||
SchematicDisplay?.SetRenderSettings(renderSettings);
|
||||
TargetSchematicDisplay?.SetRenderSettings(renderSettings);
|
||||
}
|
||||
|
||||
public void UpdatePlayer(Entity currentEntity)
|
||||
{
|
||||
ShipPanel?.Display(currentEntity, true);
|
||||
SchematicDisplay?.ShowShip(currentEntity);
|
||||
}
|
||||
|
||||
public void UpdateTarget(Entity target, Entity currentEntity)
|
||||
{
|
||||
if (TargetIndicator != null)
|
||||
TargetIndicator.gameObject.SetActive(target != null);
|
||||
if (TargetShipPanel != null)
|
||||
TargetShipPanel.gameObject.SetActive(target != null);
|
||||
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
TargetShipPanel?.Display(target, true);
|
||||
TargetSchematicDisplay?.ShowShip(target, currentEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 027535b4501541f4aa60df9ae50d9eed
|
||||
87
Assets/Scripts/Gameplay/AetheriaUnityCurrentEntityBinder.cs
Normal file
87
Assets/Scripts/Gameplay/AetheriaUnityCurrentEntityBinder.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering.PostProcessing;
|
||||
using float3 = Unity.Mathematics.float3;
|
||||
|
||||
public sealed class AetheriaUnityCurrentEntityBinder
|
||||
{
|
||||
public ZoneRenderer ZoneRenderer { get; set; }
|
||||
public PostProcessVolume DeathPost { get; set; }
|
||||
public CanvasGroup GameplayUI { get; set; }
|
||||
public AetheriaUnityObservedDockingIndex ObservedDocking { get; set; }
|
||||
public AetheriaUnityCurrentEntityPresentation CurrentEntityPresentation { get; set; }
|
||||
public AetheriaUnityTargetPresentation TargetPresentation { get; set; }
|
||||
public Func<Entity> GetCurrentEntity { get; set; }
|
||||
public Action<Entity> SetCurrentEntity { get; set; }
|
||||
public Func<float3> GetViewDirection { get; set; }
|
||||
public Action<float3> SetViewDirection { get; set; }
|
||||
public Func<AetheriaRuntimeZoneRenderDocument> ResolveZoneRender { get; set; }
|
||||
public Action<IReadOnlyList<AetheriaUnityActionBarBinding>> ApplyActionBarBindings { get; set; }
|
||||
public Action EnablePlayerInput { get; set; }
|
||||
public Action DisablePlayerInput { get; set; }
|
||||
public Action<MusicType> PlayMusic { get; set; }
|
||||
public Action<Entity> UpdateTargetPanel { get; set; }
|
||||
|
||||
public void RestoreBinding(Entity currentEntity)
|
||||
{
|
||||
if (ObservedDocking != null &&
|
||||
ObservedDocking.TryResolveDockingBay(currentEntity, out var dockParent, out _))
|
||||
{
|
||||
SetCurrentEntity?.Invoke(currentEntity);
|
||||
ApplyActionBarBindings?.Invoke(Array.Empty<AetheriaUnityActionBarBinding>());
|
||||
CurrentEntityPresentation?.BindDocked(
|
||||
dockParent,
|
||||
null,
|
||||
ResolveZoneRender?.Invoke());
|
||||
return;
|
||||
}
|
||||
|
||||
BindUndocked(
|
||||
currentEntity);
|
||||
}
|
||||
|
||||
public void ClearBinding()
|
||||
{
|
||||
TargetPresentation?.ClearIndicators();
|
||||
CurrentEntityPresentation?.ClearBinding();
|
||||
DisablePlayerInput?.Invoke();
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
if (GameplayUI != null)
|
||||
GameplayUI.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void BindUndocked(Entity entity)
|
||||
{
|
||||
if (ZoneRenderer == null ||
|
||||
!ZoneRenderer.TryGetEntityInstance(entity, out var entityInstance))
|
||||
{
|
||||
Debug.LogError($"Attempted to bind to entity {entity?.Name}, but ZoneRenderer has no daemon-indexed instance.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetCurrentEntity?.Invoke(entity);
|
||||
if (DeathPost != null)
|
||||
DeathPost.weight = 0;
|
||||
|
||||
if (CultMath.math.length(entity.CultDirection) > .1f)
|
||||
SetViewDirection?.Invoke((float3)AetheriaMath.ToUnityXZ(entity.CultDirection));
|
||||
|
||||
CurrentEntityPresentation?.BindUndocked(
|
||||
entity,
|
||||
entityInstance,
|
||||
Array.Empty<AetheriaUnityActionBarBinding>(),
|
||||
EnablePlayerInput,
|
||||
PlayMusic,
|
||||
SetCurrentEntity,
|
||||
UpdateTargetPanel,
|
||||
() => TargetPresentation?.ReconcileVisibleTargetIndicators(GetCurrentEntity?.Invoke()),
|
||||
ApplyActionBarBindings);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c599c7371ae28e249bcf132744d4e547
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cinemachine;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class AetheriaUnityCurrentEntityPresentation
|
||||
{
|
||||
private readonly List<IDisposable> _shipSubscriptions = new List<IDisposable>();
|
||||
private readonly List<IDisposable> _targetSubscriptions = new List<IDisposable>();
|
||||
private float _hitMarkerTime;
|
||||
|
||||
public ZoneRenderer ZoneRenderer { get; set; }
|
||||
public CinemachineVirtualCamera DockCamera { get; set; }
|
||||
public CinemachineVirtualCamera FollowCamera { get; set; }
|
||||
public MenuPanel Menu { get; set; }
|
||||
public TradeMenu TradeMenu { get; set; }
|
||||
public CanvasGroup GameplayUI { get; set; }
|
||||
public InventoryPanel ShipPanel { get; set; }
|
||||
public InventoryPanel TargetShipPanel { get; set; }
|
||||
public SchematicDisplay SchematicDisplay { get; set; }
|
||||
public SchematicDisplay TargetSchematicDisplay { get; set; }
|
||||
public ImageTargetPresentation TargetImages { get; set; }
|
||||
public AetheriaRuntimeCatalogSnapshot RuntimeCatalog { get; set; }
|
||||
public PlaceUIElementWorldspace[] Crosshairs { get; set; } = Array.Empty<PlaceUIElementWorldspace>();
|
||||
public Prototype LockIndicator { get; set; }
|
||||
public GameObject HitMarker { get; set; }
|
||||
public float HitMarkerDuration { get; set; }
|
||||
|
||||
public (HardpointData[] hardpoints, Transform[] barrels, PlaceUIElementWorldspace crosshair)[] ArticulationGroups { get; private set; } =
|
||||
Array.Empty<(HardpointData[] hardpoints, Transform[] barrels, PlaceUIElementWorldspace crosshair)>();
|
||||
|
||||
public (LockWeapon targetLock, PlaceUIElementWorldspace indicator, Rotate spin)[] LockingIndicators { get; private set; } =
|
||||
Array.Empty<(LockWeapon targetLock, PlaceUIElementWorldspace indicator, Rotate spin)>();
|
||||
|
||||
public void BindUndocked(
|
||||
Entity entity,
|
||||
EntityInstance entityInstance,
|
||||
IReadOnlyList<AetheriaUnityActionBarBinding> actionBarBindings,
|
||||
Action enablePlayerInput,
|
||||
Action<MusicType> playMusic,
|
||||
Action<Entity> updateCurrentEntity,
|
||||
Action<Entity> updateTargetPanel,
|
||||
Action reconcileVisibleTargetIndicators,
|
||||
Action<IReadOnlyList<AetheriaUnityActionBarBinding>> applyActionBarBindings)
|
||||
{
|
||||
if (entity == null || entityInstance == null)
|
||||
return;
|
||||
|
||||
updateCurrentEntity?.Invoke(entity);
|
||||
ZoneRenderer.PerspectiveEntity = entity;
|
||||
|
||||
Menu.gameObject.SetActive(false);
|
||||
DockCamera.enabled = false;
|
||||
FollowCamera.enabled = true;
|
||||
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
enablePlayerInput?.Invoke();
|
||||
GameplayUI.gameObject.SetActive(true);
|
||||
ShipPanel.Display(entity, true);
|
||||
SchematicDisplay.ShowShip(entity);
|
||||
|
||||
FollowCamera.LookAt = entityInstance.LookAtPoint;
|
||||
FollowCamera.Follow = entityInstance.transform;
|
||||
ArticulationGroups = entity.Equipment
|
||||
.Where(HasArticulatedWeaponBehavior)
|
||||
.GroupBy(item => entityInstance
|
||||
.GetBarrel(entity.Hardpoints[item.Position.x, item.Position.y])
|
||||
.GetComponentInParent<ArticulationPoint>()?.Group ?? -1)
|
||||
.Select((group, index) =>
|
||||
{
|
||||
return (
|
||||
group.Select(item => entity.Hardpoints[item.Position.x, item.Position.y]).ToArray(),
|
||||
group.Select(item => entityInstance.GetBarrel(entity.Hardpoints[item.Position.x, item.Position.y])).ToArray(),
|
||||
Crosshairs[index]
|
||||
);
|
||||
}).ToArray();
|
||||
|
||||
foreach (var crosshair in Crosshairs)
|
||||
crosshair.gameObject.SetActive(false);
|
||||
foreach (var group in ArticulationGroups)
|
||||
group.crosshair.gameObject.SetActive(true);
|
||||
|
||||
_shipSubscriptions.Add(entity.TargetedByCount.Subscribe(count =>
|
||||
{
|
||||
playMusic?.Invoke(count > 0 ? MusicType.Combat : MusicType.Overworld);
|
||||
}));
|
||||
|
||||
_shipSubscriptions.Add(entity.Target.Subscribe(target =>
|
||||
{
|
||||
ClearTargetSubscriptions();
|
||||
updateTargetPanel?.Invoke(target);
|
||||
ConfigureTargetShieldPresentation(target);
|
||||
if (target != null)
|
||||
{
|
||||
_targetSubscriptions.Add(target.IncomingHit.Where(e => e == entity).Subscribe(_ =>
|
||||
{
|
||||
HitMarker.SetActive(true);
|
||||
_hitMarkerTime = HitMarkerDuration;
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
reconcileVisibleTargetIndicators?.Invoke();
|
||||
|
||||
LockingIndicators = entity.GetBehaviors<LockWeapon>()
|
||||
.Select(x =>
|
||||
{
|
||||
var indicator = LockIndicator.Instantiate<PlaceUIElementWorldspace>();
|
||||
return (x, indicator, indicator.GetComponent<Rotate>());
|
||||
}).ToArray();
|
||||
|
||||
applyActionBarBindings?.Invoke(actionBarBindings ?? Array.Empty<AetheriaUnityActionBarBinding>());
|
||||
}
|
||||
|
||||
public bool HasArticulatedWeaponBehavior(EquippedItem item)
|
||||
{
|
||||
return AetheriaUnityWeaponPresentationPolicy.HasArticulatedWeaponBehavior(RuntimeCatalog, item);
|
||||
}
|
||||
|
||||
public void BindDocked(
|
||||
Entity entity,
|
||||
AetheriaRuntimeCurrentDockingDocument docking,
|
||||
AetheriaRuntimeZoneRenderDocument zoneRender)
|
||||
{
|
||||
if (entity == null)
|
||||
return;
|
||||
|
||||
TradeMenu.Inventory = entity.CargoBays.First();
|
||||
ZoneRenderer.PerspectiveEntity = entity;
|
||||
DockCamera.enabled = true;
|
||||
FollowCamera.enabled = false;
|
||||
var orbital = entity as OrbitalEntity;
|
||||
if (orbital == null || !ZoneRenderer.TryGetEntityInstance(orbital, out var orbitalInstance))
|
||||
{
|
||||
Debug.LogError($"Attempted to dock at entity {entity.Name}, but ZoneRenderer has no daemon-indexed instance.");
|
||||
return;
|
||||
}
|
||||
|
||||
DockCamera.Follow = orbitalInstance.transform;
|
||||
var parentOrbitPlanetBodyKey = ResolveDockParentBodyKey(orbital, docking, zoneRender);
|
||||
if (ZoneRenderer.TryGetBodyView(parentOrbitPlanetBodyKey, out var parentBodyView))
|
||||
DockCamera.LookAt = parentBodyView.Body.transform;
|
||||
else DockCamera.LookAt = ZoneRenderer.ZoneRoot;
|
||||
Menu.ShowTab(MenuTab.Inventory);
|
||||
}
|
||||
|
||||
private static string ResolveDockParentBodyKey(
|
||||
OrbitalEntity orbital,
|
||||
AetheriaRuntimeCurrentDockingDocument docking,
|
||||
AetheriaRuntimeZoneRenderDocument zoneRender)
|
||||
{
|
||||
if (orbital == null)
|
||||
return "";
|
||||
|
||||
var bodyPoses = zoneRender?.BodyPoses ?? Array.Empty<AetheriaRuntimeZoneRenderBodyPose>();
|
||||
var parentOrbitKey = bodyPoses
|
||||
.FirstOrDefault(body => body != null &&
|
||||
string.Equals(body.OrbitKey ?? "", orbital.OrbitKey, StringComparison.Ordinal))
|
||||
?.ParentOrbitKey ?? docking?.DockParentParentOrbitKey ?? "";
|
||||
return bodyPoses
|
||||
.FirstOrDefault(body => body != null &&
|
||||
string.Equals(body.OrbitKey ?? "", parentOrbitKey, StringComparison.Ordinal))
|
||||
?.BodyKey ?? docking?.DockParentParentBodyKey ?? "";
|
||||
}
|
||||
|
||||
public void ClearBinding()
|
||||
{
|
||||
foreach (var (_, indicator, _) in LockingIndicators)
|
||||
indicator.GetComponent<Prototype>().ReturnToPool();
|
||||
LockingIndicators = Array.Empty<(LockWeapon targetLock, PlaceUIElementWorldspace indicator, Rotate spin)>();
|
||||
ArticulationGroups = Array.Empty<(HardpointData[] hardpoints, Transform[] barrels, PlaceUIElementWorldspace crosshair)>();
|
||||
|
||||
foreach (var subscription in _shipSubscriptions)
|
||||
subscription.Dispose();
|
||||
_shipSubscriptions.Clear();
|
||||
ClearTargetSubscriptions();
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
_hitMarkerTime -= deltaTime;
|
||||
if (HitMarker.activeSelf && _hitMarkerTime < 0)
|
||||
HitMarker.SetActive(false);
|
||||
}
|
||||
|
||||
private void ClearTargetSubscriptions()
|
||||
{
|
||||
foreach (var subscription in _targetSubscriptions)
|
||||
subscription.Dispose();
|
||||
_targetSubscriptions.Clear();
|
||||
}
|
||||
|
||||
private void ConfigureTargetShieldPresentation(Entity target)
|
||||
{
|
||||
if (target == null || TargetImages == null)
|
||||
return;
|
||||
|
||||
if (target.Shield != null)
|
||||
{
|
||||
TargetImages.TargetShieldsBackground.color = new Color(TargetImages.ShieldColor.r, TargetImages.ShieldColor.g, TargetImages.ShieldColor.b, .4f);
|
||||
TargetImages.TargetShieldsIcon.color = TargetImages.ShieldColor;
|
||||
TargetImages.TargetShieldsIcon.sprite = TargetImages.ShieldIcon;
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetImages.TargetShieldsBackground.color = new Color(TargetImages.NoShieldColor.r, TargetImages.NoShieldColor.g, TargetImages.NoShieldColor.b, .4f);
|
||||
TargetImages.TargetShieldsIcon.color = TargetImages.NoShieldColor;
|
||||
TargetImages.TargetShieldsIcon.sprite = TargetImages.NoShieldIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ImageTargetPresentation
|
||||
{
|
||||
public UnityEngine.UI.Image TargetShieldsBackground { get; set; }
|
||||
public UnityEngine.UI.Image TargetShieldsIcon { get; set; }
|
||||
public Color ShieldColor { get; set; }
|
||||
public Color NoShieldColor { get; set; }
|
||||
public Sprite ShieldIcon { get; set; }
|
||||
public Sprite NoShieldIcon { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f5a7b36e50785fc45ab94ae5a03dd1d6
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using Regex = System.Text.RegularExpressions.Regex;
|
||||
|
||||
public static class AetheriaUnityDaemonEntitySnapshotProjector
|
||||
{
|
||||
public static AetheriaRuntimeEntitySnapshot[] CreateSnapshots(
|
||||
string runId,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone)
|
||||
{
|
||||
if (zone == null)
|
||||
return Array.Empty<AetheriaRuntimeEntitySnapshot>();
|
||||
|
||||
return CreateSnapshots(runId, zone.ZoneIndex, zone.Entities);
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeEntitySnapshot[] CreateSnapshots(
|
||||
string runId,
|
||||
int zoneIndex,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities)
|
||||
{
|
||||
return (entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.Where(entity => entity != null)
|
||||
.Select(entity => CreateSnapshot(runId, zoneIndex, entity))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static int EntityIndexFromRecordKey(string recordKey)
|
||||
{
|
||||
var match = Regex.Match(recordKey ?? "", @"\.entity\.(\d+)\.v1$");
|
||||
return match.Success && int.TryParse(match.Groups[1].Value, out var index)
|
||||
? index
|
||||
: int.MaxValue;
|
||||
}
|
||||
|
||||
public static string DaemonEntityRecordKey(string runId, int zoneIndex, int entityIndex)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(runId)
|
||||
? ""
|
||||
: $"global:aetheria.run_state.{runId}.zone.{zoneIndex}.entity.{entityIndex}.v1";
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeEntitySnapshot CreateSnapshot(
|
||||
string runId,
|
||||
int zoneIndex,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity)
|
||||
{
|
||||
return new AetheriaRuntimeEntitySnapshot(
|
||||
DaemonEntityRecordKey(runId, zoneIndex, entity.EntityIndex),
|
||||
entity.Name ?? "",
|
||||
entity.Kind ?? "",
|
||||
entity.PositionX,
|
||||
entity.PositionY,
|
||||
entity.PositionZ,
|
||||
entity.DirectionX,
|
||||
entity.DirectionY,
|
||||
entity.FactionKey ?? "",
|
||||
entity.HullItemKey ?? "",
|
||||
CreateItemSlots(entity.Equipment),
|
||||
CreateItemSlots(entity.CargoBays),
|
||||
CreateItemSlots(entity.DockingBays),
|
||||
(entity.ChildEntityIndices ?? Array.Empty<int>())
|
||||
.Where(index => index >= 0)
|
||||
.Select(index => DaemonEntityRecordKey(runId, zoneIndex, index))
|
||||
.ToArray(),
|
||||
(entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>())
|
||||
.Select(group => (IReadOnlyList<int>)(group ?? Array.Empty<int>()).ToArray())
|
||||
.ToArray(),
|
||||
(entity.StatGrids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>())
|
||||
.Select(grid => new AetheriaRuntimeEntityStatGridSnapshot(
|
||||
grid.Name ?? "",
|
||||
grid.Width,
|
||||
grid.Height,
|
||||
(grid.Values ?? Array.Empty<double>()).ToArray()))
|
||||
.ToArray(),
|
||||
entity.VelocityX,
|
||||
entity.VelocityY,
|
||||
entity.TargetEntityIndex < 0 ? "" : DaemonEntityRecordKey(runId, zoneIndex, entity.TargetEntityIndex),
|
||||
entity.IsActive,
|
||||
entity.HeatsinksEnabled,
|
||||
entity.OverrideShutdown,
|
||||
entity.TractorPower,
|
||||
entity.Heatstroke,
|
||||
entity.Hypothermia,
|
||||
(entity.ActiveConsumables ?? Array.Empty<AetheriaRuntimeActiveConsumableCommit>())
|
||||
.Select(consumable => new AetheriaRuntimeActiveConsumableSnapshot(
|
||||
consumable.ItemKey ?? "",
|
||||
consumable.Quality,
|
||||
consumable.RemainingDuration,
|
||||
consumable.Duration))
|
||||
.ToArray(),
|
||||
(entity.BehaviorProgress ?? Array.Empty<AetheriaRuntimeBehaviorProgressCommit>())
|
||||
.Select(progress => new AetheriaRuntimeBehaviorProgressSnapshot(
|
||||
progress.OwnerKind ?? "",
|
||||
progress.OwnerIndex,
|
||||
progress.BehaviorIndex,
|
||||
progress.BehaviorKind ?? "",
|
||||
progress.Progress))
|
||||
.ToArray(),
|
||||
CreateWeaponStates(runId, zoneIndex, entity.WeaponStates),
|
||||
CreateBehaviorStates(entity.BehaviorStates),
|
||||
CreateCargoBays(entity.CargoContents),
|
||||
CreateCargoBays(entity.DockingBayContents),
|
||||
(entity.DockingBayAssignments ?? Array.Empty<int>()).ToArray(),
|
||||
entity.Visibility,
|
||||
entity.VisibilitySourceCount,
|
||||
(entity.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
|
||||
.Where(contact => contact != null && contact.TargetEntityIndex >= 0)
|
||||
.Select(contact => new AetheriaRuntimeEntityContactSnapshot(
|
||||
DaemonEntityRecordKey(runId, zoneIndex, contact.TargetEntityIndex),
|
||||
contact.InfoGathered,
|
||||
contact.Hostile,
|
||||
contact.Visible))
|
||||
.ToArray(),
|
||||
entity.ShutdownPerformance);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeEntityItemSlotSnapshot[] CreateItemSlots(
|
||||
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> slots)
|
||||
{
|
||||
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Where(slot => slot?.Item != null)
|
||||
.Select(slot => new AetheriaRuntimeEntityItemSlotSnapshot(
|
||||
slot.X,
|
||||
slot.Y,
|
||||
slot.Item.ItemKey ?? "",
|
||||
slot.Item.Quality,
|
||||
slot.Item.Durability,
|
||||
slot.Item.Quantity,
|
||||
slot.Item.Enabled,
|
||||
slot.Item.OverrideShutdown,
|
||||
slot.Item.Temperature))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCargoBayLoadoutSnapshot[] CreateCargoBays(
|
||||
IReadOnlyList<AetheriaRuntimeCargoBayLoadoutCommit> cargoBays)
|
||||
{
|
||||
return (cargoBays ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
||||
.Select(bay => new AetheriaRuntimeCargoBayLoadoutSnapshot(
|
||||
CreateLoadoutSlots(bay?.Items)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeLoadoutItemSlotSnapshot[] CreateLoadoutSlots(
|
||||
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit> slots)
|
||||
{
|
||||
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.Where(slot => slot?.Item != null)
|
||||
.Select(slot => new AetheriaRuntimeLoadoutItemSlotSnapshot(
|
||||
slot.X,
|
||||
slot.Y,
|
||||
new AetheriaRuntimeLoadoutItemSnapshot(
|
||||
slot.Item.ItemKey ?? "",
|
||||
slot.Item.Quality,
|
||||
slot.Item.Durability,
|
||||
slot.Item.Quantity,
|
||||
slot.Item.Enabled,
|
||||
slot.Item.OverrideShutdown,
|
||||
slot.Item.Temperature)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeWeaponStateSnapshot[] CreateWeaponStates(
|
||||
string runId,
|
||||
int zoneIndex,
|
||||
IReadOnlyList<AetheriaRuntimeWeaponStateCommit> weaponStates)
|
||||
{
|
||||
return (weaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>())
|
||||
.Where(state => state != null)
|
||||
.Select(state => new AetheriaRuntimeWeaponStateSnapshot(
|
||||
state.OwnerKind ?? "",
|
||||
state.OwnerIndex,
|
||||
state.BehaviorIndex,
|
||||
state.BehaviorKind ?? "",
|
||||
state.Firing,
|
||||
state.Ammo,
|
||||
state.BurstRemaining,
|
||||
state.BurstTimer,
|
||||
state.BurstInterval,
|
||||
state.CooldownProgress,
|
||||
state.CoolingDown,
|
||||
state.Charging,
|
||||
state.Charged,
|
||||
state.Charge,
|
||||
state.Reloading,
|
||||
state.ReloadProgress,
|
||||
state.AmmoIntervalProgress,
|
||||
state.LockProgress,
|
||||
state.LockTargetEntityIndex < 0 ? "" : DaemonEntityRecordKey(runId, zoneIndex, state.LockTargetEntityIndex)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorStateSnapshot[] CreateBehaviorStates(
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorStateCommit> behaviorStates)
|
||||
{
|
||||
return (behaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(state => state != null)
|
||||
.Select(state => new AetheriaRuntimeBehaviorStateSnapshot(
|
||||
state.OwnerKind ?? "",
|
||||
state.OwnerIndex,
|
||||
state.BehaviorIndex,
|
||||
state.BehaviorKind ?? "",
|
||||
state.Pinging,
|
||||
state.PingCooldown,
|
||||
state.PingLerp,
|
||||
state.PingRadius,
|
||||
state.PingedEntityCount,
|
||||
state.RadiatorTemperature,
|
||||
state.Emissivity,
|
||||
state.PumpedHeat,
|
||||
state.WasteHeat,
|
||||
state.EnergyUsage,
|
||||
state.ReactorDraw,
|
||||
state.ReactorLoadRatio,
|
||||
state.CapacitorCharge,
|
||||
state.CapacitorCapacity,
|
||||
state.CapacitorEfficiency,
|
||||
state.AetherDriveAxisX,
|
||||
state.AetherDriveAxisY,
|
||||
state.AetherDriveAxisZ,
|
||||
state.AetherDriveThrustX,
|
||||
state.AetherDriveThrustY,
|
||||
state.AetherDriveThrustZ,
|
||||
state.AetherDriveRpmX,
|
||||
state.AetherDriveRpmY,
|
||||
state.AetherDriveRpmZ,
|
||||
state.AetherDriveMaximumRpm,
|
||||
state.AetherDriveThrustDirectionX,
|
||||
state.AetherDriveThrustDirectionY,
|
||||
state.ResourceScannerTargetBodyKey ?? "",
|
||||
state.ResourceScannerAsteroidIndex,
|
||||
state.ResourceScannerScanTime,
|
||||
state.ResourceScannerRange,
|
||||
state.ResourceScannerMinimumDensity,
|
||||
state.ResourceScannerScanDuration,
|
||||
state.MiningToolAsteroidBeltKey ?? "",
|
||||
state.MiningToolAsteroidIndex,
|
||||
state.MiningToolRange,
|
||||
state.ThrusterAxis,
|
||||
state.ThrusterThrust,
|
||||
state.ThrusterTorque,
|
||||
state.ShieldEfficiency,
|
||||
state.ShieldEnergyUsage,
|
||||
state.VelocityLimit,
|
||||
state.ThermotoggleTargetTemperature,
|
||||
state.SwitchActivated,
|
||||
state.TriggerPulled,
|
||||
state.StatModifierApplied,
|
||||
state.StatModifierExecuted,
|
||||
state.StatModifierTargetStatCount,
|
||||
state.TurretControllerWeaponCount,
|
||||
state.TurretControllerShotSpeed,
|
||||
state.TurretControllerPredictShots))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e81930c7560adfe48b4f959791ab3572
|
||||
45
Assets/Scripts/Gameplay/AetheriaUnityDragObjects.cs
Normal file
45
Assets/Scripts/Gameplay/AetheriaUnityDragObjects.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using int2 = Unity.Mathematics.int2;
|
||||
|
||||
public abstract class DragObject
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class ItemDragObject : DragObject
|
||||
{
|
||||
protected ItemDragObject(int2 originCellOffset, ItemInstance item)
|
||||
{
|
||||
OriginCellOffset = originCellOffset;
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public ItemInstance Item { get; }
|
||||
public int2 OriginCellOffset { get; }
|
||||
}
|
||||
|
||||
public class ItemInstanceDragObject : ItemDragObject
|
||||
{
|
||||
public ItemInstanceDragObject(ItemInstance item, EquippedCargoBay originInventory, int2 originCellOffset)
|
||||
: base(originCellOffset, item)
|
||||
{
|
||||
OriginInventory = originInventory;
|
||||
}
|
||||
|
||||
public EquippedCargoBay OriginInventory { get; }
|
||||
}
|
||||
|
||||
public class EquippedItemDragObject : ItemDragObject
|
||||
{
|
||||
public EquippedItemDragObject(EquippedItem item, Entity originEntity, int2 originCellOffset)
|
||||
: base(originCellOffset, item.EquippableItem)
|
||||
{
|
||||
EquippedItem = item;
|
||||
OriginEntity = originEntity;
|
||||
}
|
||||
|
||||
public EquippedItem EquippedItem { get; }
|
||||
public Entity OriginEntity { get; }
|
||||
}
|
||||
2
Assets/Scripts/Gameplay/AetheriaUnityDragObjects.cs.meta
Normal file
2
Assets/Scripts/Gameplay/AetheriaUnityDragObjects.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5f70b0c04a804c349915b07ef0fd9b13
|
||||
37
Assets/Scripts/Gameplay/AetheriaUnityDragSession.cs
Normal file
37
Assets/Scripts/Gameplay/AetheriaUnityDragSession.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
|
||||
public sealed class AetheriaUnityDragSession
|
||||
{
|
||||
private DragObject _dragObject;
|
||||
private Action<DragObject> _endDragCallback;
|
||||
|
||||
public void Begin(DragObject dragObject)
|
||||
{
|
||||
_dragObject = dragObject;
|
||||
}
|
||||
|
||||
public bool TryGetDraggedItem(out ItemDragObject itemDragObject)
|
||||
{
|
||||
itemDragObject = _dragObject as ItemDragObject;
|
||||
return itemDragObject != null;
|
||||
}
|
||||
|
||||
public void RegisterTarget(Action<DragObject> onEndDrag)
|
||||
{
|
||||
_endDragCallback = onEndDrag;
|
||||
}
|
||||
|
||||
public void UnregisterTarget()
|
||||
{
|
||||
_endDragCallback = null;
|
||||
}
|
||||
|
||||
public bool End()
|
||||
{
|
||||
var hadTarget = _endDragCallback != null;
|
||||
_endDragCallback?.Invoke(_dragObject);
|
||||
_dragObject = null;
|
||||
_endDragCallback = null;
|
||||
return hadTarget;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Gameplay/AetheriaUnityDragSession.cs.meta
Normal file
2
Assets/Scripts/Gameplay/AetheriaUnityDragSession.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6454eeaf62eadb44abbe5d54bf497312
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using Unity.Mathematics;
|
||||
|
||||
public sealed class AetheriaUnityEntityConstructionBlueprintProjector
|
||||
{
|
||||
private readonly AetheriaUnityLoadoutItemProjector _loadoutItemProjector;
|
||||
|
||||
public AetheriaUnityEntityConstructionBlueprintProjector(
|
||||
AetheriaUnityLoadoutItemProjector loadoutItemProjector)
|
||||
{
|
||||
_loadoutItemProjector = loadoutItemProjector ?? throw new ArgumentNullException(nameof(loadoutItemProjector));
|
||||
}
|
||||
|
||||
public EntityConstructionBlueprint ProjectTemplate(AetheriaRuntimeLoadoutTemplateSnapshot template)
|
||||
{
|
||||
if (template == null)
|
||||
return null;
|
||||
|
||||
var blueprint = ProjectLoadoutEntity(template.RootEntity);
|
||||
if (blueprint != null && string.IsNullOrWhiteSpace(blueprint.Name))
|
||||
blueprint.Name = template.Name;
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
public EntityConstructionBlueprint ProjectLoadoutEntity(AetheriaRuntimeEntityLoadoutSnapshot entity)
|
||||
{
|
||||
if (entity == null)
|
||||
return null;
|
||||
|
||||
var hull = CreateEquippableItem(entity.Hull);
|
||||
if (hull == null)
|
||||
return null;
|
||||
|
||||
var blueprint = CreateBlueprint(entity.Kind);
|
||||
if (blueprint is ShipConstructionBlueprint shipBlueprint)
|
||||
{
|
||||
shipBlueprint.Direction = new float2(0, 1);
|
||||
}
|
||||
|
||||
blueprint.Name = entity.Name;
|
||||
blueprint.FactionKey = entity.FactionKey ?? "";
|
||||
blueprint.Hull = hull;
|
||||
blueprint.Equipment = CreateEquippableSlots(entity.Equipment);
|
||||
blueprint.CargoBays = CreateEquippableSlots(entity.CargoBays);
|
||||
blueprint.DockingBays = CreateEquippableSlots(entity.DockingBays);
|
||||
blueprint.CargoContents = CreateCargoBayContents(entity.CargoContents);
|
||||
blueprint.DockingBayContents = CreateCargoBayContents(entity.DockingBayContents);
|
||||
blueprint.DockingBayAssignments = entity.DockingBayAssignments.ToArray();
|
||||
blueprint.WeaponGroups = entity.WeaponGroups.Select(group => group.ToArray()).ToArray();
|
||||
blueprint.Children = entity.Children
|
||||
.Select(ProjectLoadoutEntity)
|
||||
.Where(child => child != null)
|
||||
.ToArray();
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
public EntityConstructionBlueprint ProjectObservedEntity(
|
||||
AetheriaRuntimeEntitySnapshot entity,
|
||||
bool isCurrentEntity)
|
||||
{
|
||||
if (entity == null)
|
||||
return null;
|
||||
|
||||
var hull = CreateEquippableItem(new AetheriaRuntimeLoadoutItemSnapshot(
|
||||
entity.HullItemKey,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
true,
|
||||
false));
|
||||
if (hull == null)
|
||||
return null;
|
||||
|
||||
var blueprint = CreateBlueprint(entity.Kind);
|
||||
if (blueprint is ShipConstructionBlueprint shipBlueprint)
|
||||
{
|
||||
shipBlueprint.Position = new float3((float)entity.PositionX, (float)entity.PositionY, (float)entity.PositionZ);
|
||||
shipBlueprint.Direction = new float2((float)entity.DirectionX, (float)entity.DirectionY);
|
||||
shipBlueprint.IsPlayerShip = isCurrentEntity;
|
||||
}
|
||||
|
||||
blueprint.Name = entity.Name ?? "";
|
||||
blueprint.FactionKey = entity.FactionKey ?? "";
|
||||
blueprint.Hull = hull;
|
||||
blueprint.Equipment = CreateEquippableSlots(entity.Equipment);
|
||||
blueprint.CargoBays = CreateEquippableSlots(entity.CargoBays);
|
||||
blueprint.DockingBays = CreateEquippableSlots(entity.DockingBays);
|
||||
blueprint.CargoContents = CreateCargoBayContents(entity.CargoContents);
|
||||
blueprint.DockingBayContents = CreateCargoBayContents(entity.DockingBayContents);
|
||||
blueprint.DockingBayAssignments = entity.DockingBayAssignments.ToArray();
|
||||
blueprint.WeaponGroups = entity.WeaponGroups.Select(group => group.ToArray()).ToArray();
|
||||
blueprint.Children = Array.Empty<EntityConstructionBlueprint>();
|
||||
return blueprint;
|
||||
}
|
||||
|
||||
private static EntityConstructionBlueprint CreateBlueprint(string kind)
|
||||
{
|
||||
return string.Equals(kind, "orbital", StringComparison.OrdinalIgnoreCase)
|
||||
? new OrbitalEntityConstructionBlueprint()
|
||||
: new ShipConstructionBlueprint();
|
||||
}
|
||||
|
||||
private (int2 position, EquippableItem item)[] CreateEquippableSlots(
|
||||
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotSnapshot> slots)
|
||||
{
|
||||
return (slots ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotSnapshot>())
|
||||
.Select(slot => (position: new int2(slot.X, slot.Y), item: CreateEquippableItem(slot.Item)))
|
||||
.Where(slot => slot.item != null)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private (int2 position, EquippableItem item)[] CreateEquippableSlots(
|
||||
IReadOnlyList<AetheriaRuntimeEntityItemSlotSnapshot> slots)
|
||||
{
|
||||
return (slots ?? Array.Empty<AetheriaRuntimeEntityItemSlotSnapshot>())
|
||||
.Select(slot => (position: new int2(slot.X, slot.Y), item: CreateEquippableItem(new AetheriaRuntimeLoadoutItemSnapshot(
|
||||
slot.ItemKey,
|
||||
slot.Quality,
|
||||
slot.Durability,
|
||||
slot.Quantity,
|
||||
slot.Enabled,
|
||||
slot.OverrideShutdown,
|
||||
slot.Temperature))))
|
||||
.Where(slot => slot.item != null)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private (int2 position, ItemInstance item)[][] CreateCargoBayContents(
|
||||
IReadOnlyList<AetheriaRuntimeCargoBayLoadoutSnapshot> bays)
|
||||
{
|
||||
return (bays ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutSnapshot>())
|
||||
.Select(bay => (bay.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotSnapshot>())
|
||||
.Select(slot => (position: new int2(slot.X, slot.Y), item: _loadoutItemProjector.CreateLoadoutItem(slot.Item)))
|
||||
.Where(slot => slot.item != null)
|
||||
.ToArray())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private EquippableItem CreateEquippableItem(AetheriaRuntimeLoadoutItemSnapshot item)
|
||||
{
|
||||
var instance = _loadoutItemProjector.CreateLoadoutItem(item) as EquippableItem;
|
||||
if (instance != null && item.Durability > 0)
|
||||
instance.Durability = (float)item.Durability;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 88f0588c14c7a1d4c87095a96585c957
|
||||
117
Assets/Scripts/Gameplay/AetheriaUnityGameplayBootShell.cs
Normal file
117
Assets/Scripts/Gameplay/AetheriaUnityGameplayBootShell.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
|
||||
public sealed class AetheriaUnityGameplayBootShell
|
||||
{
|
||||
public GameSettings Settings { get; set; }
|
||||
public ZoneRenderer ZoneRenderer { get; set; }
|
||||
public AetheriaUnityCockpitHudShell CockpitHudShell { get; set; }
|
||||
public float TargetSpottedBlinkFrequency { get; set; }
|
||||
public float TargetSpottedBlinkOffset { get; set; }
|
||||
public Action<string> Log { get; set; }
|
||||
|
||||
public AetheriaUnityGameplayBootResult Boot()
|
||||
{
|
||||
var stateBoot = AetheriaRuntimeStateBoot.Inspect(AetheriaUnityRuntimePaths.GameDataDirectory);
|
||||
Log?.Invoke($"Aetheria runtime target: {stateBoot.TargetLabel} via {stateBoot.TargetKind} ({stateBoot.TargetSource})");
|
||||
Log?.Invoke($"Aetheria runtime id: {stateBoot.RuntimeId}");
|
||||
Log?.Invoke($"Aetheria runtime state file: {stateBoot.StateFilePath}");
|
||||
stateBoot = SyncRemoteReplicaBeforeBoot(stateBoot);
|
||||
|
||||
if (!stateBoot.SupportsLocalStateFileRead)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Aetheria runtime target cannot read the daemon mirror state required for gameplay boot: {stateBoot.FailureMessage}");
|
||||
}
|
||||
|
||||
if (!stateBoot.StateFileExists)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Aetheria runtime state file is missing at {stateBoot.StateFilePath}; gameplay requires an authoritative daemon mirror.");
|
||||
}
|
||||
|
||||
var runtimeCatalog = AetheriaUnityRuntimeClientProvider.ResolveClient(stateBoot.StateFilePath, stateBoot.RuntimeId)
|
||||
.OpenRuntimeCatalog();
|
||||
if (runtimeCatalog == null)
|
||||
throw new InvalidOperationException("Aetheria typed runtime catalog is required before gameplay boot.");
|
||||
|
||||
Log?.Invoke($"Aetheria runtime catalog: {runtimeCatalog.Items.Count} items, {runtimeCatalog.Corporations.Count} corporations, {runtimeCatalog.NameFiles.Count} name files");
|
||||
|
||||
var itemManager = new ItemManager(
|
||||
runtimeCatalog,
|
||||
Settings.GameplaySettings,
|
||||
message => Log?.Invoke(message));
|
||||
var loadoutItemProjector = new AetheriaUnityLoadoutItemProjector(itemManager, runtimeCatalog);
|
||||
ZoneRenderer.SetDroppedPickupItemProjector(loadoutItemProjector.CreateLoadoutItem);
|
||||
ZoneRenderer.BodySettingsCollections = Settings.BodySettingsCollections;
|
||||
ZoneRenderer.RenderSettings = AetheriaUnityRenderSettingsBridge.Build(
|
||||
Settings,
|
||||
TargetSpottedBlinkFrequency,
|
||||
TargetSpottedBlinkOffset);
|
||||
CockpitHudShell.SetRenderSettings(ZoneRenderer.RenderSettings);
|
||||
|
||||
if (!AetheriaUnityRuntimeClientProvider.PlayerSettings.GraphicsSettings.ShowAsteroidsInMinimap)
|
||||
ZoneRenderer.ShowAsteroidUI = false;
|
||||
|
||||
return new AetheriaUnityGameplayBootResult(
|
||||
runtimeCatalog,
|
||||
itemManager,
|
||||
loadoutItemProjector);
|
||||
}
|
||||
|
||||
private AetheriaRuntimeStateBootReport SyncRemoteReplicaBeforeBoot(AetheriaRuntimeStateBootReport stateBoot)
|
||||
{
|
||||
if (!ShouldSyncRemoteReplicaBeforeBoot(stateBoot))
|
||||
return stateBoot;
|
||||
|
||||
Log?.Invoke($"Aetheria remote Verse replica missing; syncing {stateBoot.TargetLabel} from {stateBoot.CultMeshAddress}");
|
||||
try
|
||||
{
|
||||
var target = AetheriaState.At(AetheriaUnityRuntimePaths.GameDataDirectory)
|
||||
.ClientTarget
|
||||
.SyncReplica();
|
||||
AetheriaUnityRuntimePaths.ClearRuntimeStateFilePathCache();
|
||||
|
||||
var refreshed = AetheriaRuntimeStateBoot.Inspect(AetheriaUnityRuntimePaths.GameDataDirectory);
|
||||
Log?.Invoke($"Aetheria remote Verse replica synced: {refreshed.StateFilePath}");
|
||||
if (!string.IsNullOrWhiteSpace(target.LastReplicaSyncAtUtc))
|
||||
Log?.Invoke($"Aetheria remote Verse replica sync time: {target.LastReplicaSyncAtUtc}");
|
||||
return refreshed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AetheriaUnityRuntimePaths.ClearRuntimeStateFilePathCache();
|
||||
throw new InvalidOperationException(
|
||||
$"Aetheria remote Verse replica sync failed before gameplay boot: {ex.Message}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldSyncRemoteReplicaBeforeBoot(AetheriaRuntimeStateBootReport stateBoot)
|
||||
{
|
||||
return string.Equals(stateBoot.TargetSource, "client-target", StringComparison.Ordinal)
|
||||
&& string.Equals(stateBoot.TargetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal)
|
||||
&& (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct AetheriaUnityGameplayBootResult
|
||||
{
|
||||
public AetheriaUnityGameplayBootResult(
|
||||
AetheriaRuntimeCatalogSnapshot runtimeCatalog,
|
||||
ItemManager itemManager,
|
||||
AetheriaUnityLoadoutItemProjector loadoutItemProjector)
|
||||
{
|
||||
RuntimeCatalog = runtimeCatalog;
|
||||
ItemManager = itemManager;
|
||||
LoadoutItemProjector = loadoutItemProjector;
|
||||
}
|
||||
|
||||
public AetheriaRuntimeCatalogSnapshot RuntimeCatalog { get; }
|
||||
public ItemManager ItemManager { get; }
|
||||
public AetheriaUnityLoadoutItemProjector LoadoutItemProjector { get; }
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 70318b35e2014bdfad48683869bfc04d
|
||||
218
Assets/Scripts/Gameplay/AetheriaUnityGameplayInputShell.cs
Normal file
218
Assets/Scripts/Gameplay/AetheriaUnityGameplayInputShell.cs
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TMPro;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public sealed class AetheriaUnityGameplayInputShell : IDisposable
|
||||
{
|
||||
private readonly List<ActionBarSlot> _actionBarSlots = new List<ActionBarSlot>();
|
||||
private readonly List<InputAction> _actionBarActions = new List<InputAction>();
|
||||
private bool _uiHidden;
|
||||
private int _zoomLevelIndex;
|
||||
|
||||
public AetheriaInput Input { get; private set; }
|
||||
public RuntimePlayerSettings RuntimePlayerSettings { get; set; }
|
||||
public InputDisplayLayout InputDisplayLayout { get; set; }
|
||||
public ConfirmationDialog Dialog { get; set; }
|
||||
public MainMenu MainMenu { get; set; }
|
||||
public MenuPanel Menu { get; set; }
|
||||
public MapRenderer MenuMap { get; set; }
|
||||
public CanvasGroup GameplayUI { get; set; }
|
||||
public Transform ActionBar { get; set; }
|
||||
public ActionBarSlot ActionBarSlot { get; set; }
|
||||
public ZoneRenderer ZoneRenderer { get; set; }
|
||||
public AetheriaUnityMenuShell MenuShell { get; set; }
|
||||
public AetheriaUnityDragSession DragSession { get; set; }
|
||||
public AetheriaUnityActionBarPresentation ActionBarPresentation { get; set; }
|
||||
public AetheriaUnityPilotOperationAdapter PilotOperationAdapter { get; set; }
|
||||
public Func<Entity> ResolveCurrentEntity { get; set; }
|
||||
public Action<AetheriaInput> SetInput { get; set; }
|
||||
|
||||
public IReadOnlyList<ActionBarSlot> ActionBarSlots => _actionBarSlots;
|
||||
|
||||
public void Bootstrap()
|
||||
{
|
||||
Input = new AetheriaInput();
|
||||
SetInput?.Invoke(Input);
|
||||
|
||||
if (RuntimePlayerSettings?.InputSettings != null)
|
||||
{
|
||||
foreach (var binding in RuntimePlayerSettings.InputSettings.InputActionMap)
|
||||
Input.asset[binding.Key.action].ApplyBindingOverride(binding.Key.binding, binding.Value);
|
||||
}
|
||||
|
||||
if (InputDisplayLayout != null)
|
||||
InputDisplayLayout.Input = Input.asset;
|
||||
|
||||
Dialog?.SetInputGate(
|
||||
() => Input.Global.Enable(),
|
||||
() => Input.Global.Disable());
|
||||
Input.Global.Enable();
|
||||
|
||||
_zoomLevelIndex = ZoneRenderer.RenderSettings.ResolveDefaultMinimapZoomIndex();
|
||||
ZoneRenderer.MinimapDistance = (float)ZoneRenderer.RenderSettings.ResolveMinimapDistance(_zoomLevelIndex);
|
||||
|
||||
RegisterGlobalInput();
|
||||
RegisterPlayerInput();
|
||||
RegisterActionBarInput();
|
||||
}
|
||||
|
||||
public void EnablePlayerInput()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Input.Player.Enable();
|
||||
foreach (var action in _actionBarActions)
|
||||
action.Enable();
|
||||
}
|
||||
|
||||
public void DisablePlayerInput()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Input.Player.Disable();
|
||||
foreach (var action in _actionBarActions)
|
||||
action.Disable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var action in _actionBarActions)
|
||||
action.Dispose();
|
||||
_actionBarActions.Clear();
|
||||
_actionBarSlots.Clear();
|
||||
Input?.Dispose();
|
||||
Input = null;
|
||||
}
|
||||
|
||||
private void RegisterGlobalInput()
|
||||
{
|
||||
Input.Global.ZoneMap.performed += context =>
|
||||
{
|
||||
MenuShell.ToggleMenuTab(MenuTab.Map);
|
||||
var currentEntity = ResolveCurrentEntity?.Invoke();
|
||||
if (currentEntity == null)
|
||||
return;
|
||||
|
||||
MenuMap.Position = AetheriaMath.ToUnity(currentEntity.CultPositionXZ);
|
||||
};
|
||||
|
||||
Input.Global.Inventory.performed += context => MenuShell.ToggleMenuTab(MenuTab.Inventory);
|
||||
Input.Global.GalaxyMap.performed += context => MenuShell.ToggleMenuTab(MenuTab.Galaxy);
|
||||
|
||||
Input.Global.Interact.performed += context =>
|
||||
{
|
||||
if (IsTextInputFocused() || MainMenu.gameObject.activeSelf)
|
||||
return;
|
||||
|
||||
if (ResolveCurrentEntity?.Invoke() == null)
|
||||
{
|
||||
Dialog.Clear();
|
||||
Dialog.Title.text = "Can't undock. You dont have a ship!";
|
||||
Dialog.Show();
|
||||
Dialog.MoveToCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
PilotOperationAdapter.RequestInteract();
|
||||
};
|
||||
|
||||
Input.Global.MainMenu.performed += context =>
|
||||
{
|
||||
if (Menu.gameObject.activeSelf)
|
||||
MenuShell.ToggleMenuTab(Menu.CurrentTab);
|
||||
else
|
||||
MenuShell.ToggleFullscreenMenu(MainMenu.gameObject);
|
||||
};
|
||||
|
||||
Input.Global.InputScreen.performed += context => MenuShell.ToggleFullscreenMenu(MenuShell.HelpScreen);
|
||||
}
|
||||
|
||||
private void RegisterPlayerInput()
|
||||
{
|
||||
Input.Player.MinimapZoom.performed += context =>
|
||||
{
|
||||
_zoomLevelIndex = ZoneRenderer.RenderSettings.ResolveNextMinimapZoomIndex(_zoomLevelIndex);
|
||||
ZoneRenderer.MinimapDistance = (float)ZoneRenderer.RenderSettings.ResolveMinimapDistance(_zoomLevelIndex);
|
||||
};
|
||||
|
||||
Input.Player.HideUI.performed += context =>
|
||||
{
|
||||
_uiHidden = !_uiHidden;
|
||||
GameplayUI.alpha = _uiHidden ? 0 : 1;
|
||||
ActionBar.gameObject.SetActive(!_uiHidden);
|
||||
};
|
||||
|
||||
Input.Player.OverrideShutdown.performed += context =>
|
||||
{
|
||||
var currentEntity = ResolveCurrentEntity?.Invoke();
|
||||
if (currentEntity != null)
|
||||
PilotOperationAdapter.RequestOverrideShutdown(!currentEntity.OverrideShutdown);
|
||||
};
|
||||
|
||||
Input.Player.Ping.performed += context => PilotOperationAdapter.RequestSensorPing();
|
||||
Input.Player.ToggleHeatsinks.performed += context =>
|
||||
{
|
||||
var currentEntity = ResolveCurrentEntity?.Invoke();
|
||||
if (currentEntity != null)
|
||||
PilotOperationAdapter.RequestHeatsinksEnabled(!currentEntity.HeatsinksEnabled);
|
||||
};
|
||||
Input.Player.ToggleShield.performed += context => PilotOperationAdapter.RequestShieldToggle();
|
||||
Input.Player.TargetReticle.performed += context => PilotOperationAdapter.RequestTargetReticle();
|
||||
Input.Player.TargetNearest.performed += context => PilotOperationAdapter.RequestTargetNearest();
|
||||
Input.Player.TargetNext.performed += context => PilotOperationAdapter.RequestTargetNext();
|
||||
Input.Player.TargetPrevious.performed += context => PilotOperationAdapter.RequestTargetPrevious();
|
||||
}
|
||||
|
||||
private void RegisterActionBarInput()
|
||||
{
|
||||
foreach (var controlPath in (RuntimePlayerSettings?.InputSettings?.ActionBarInputs ?? Enumerable.Empty<string>()).OrderBy(input => input))
|
||||
CreateActionBarSlot(controlPath);
|
||||
}
|
||||
|
||||
private void CreateActionBarSlot(string controlPath)
|
||||
{
|
||||
var action = new InputAction(binding: controlPath);
|
||||
_actionBarActions.Add(action);
|
||||
|
||||
var slot = UnityEngine.Object.Instantiate(ActionBarSlot, ActionBar);
|
||||
slot.ControlPath = controlPath;
|
||||
slot.Binding = null;
|
||||
_actionBarSlots.Add(slot);
|
||||
|
||||
action.started += context => slot.Binding?.Activate();
|
||||
action.canceled += context => slot.Binding?.Deactivate();
|
||||
|
||||
var shortName = controlPath.Substring(controlPath.LastIndexOf('/') + 1);
|
||||
var sprite = Resources.Load<Sprite>($"Sprites/Input/{shortName}");
|
||||
if (sprite != null)
|
||||
{
|
||||
slot.InputIcon.sprite = sprite;
|
||||
slot.InputLabel.gameObject.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
slot.InputLabel.text = shortName;
|
||||
slot.InputIcon.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
slot.PointerEnterTrigger.OnPointerEnterAsObservable().Subscribe(_ =>
|
||||
{
|
||||
DragSession.RegisterTarget(dragAction => ActionBarPresentation.RequestBinding(slot, dragAction));
|
||||
});
|
||||
slot.PointerExitTrigger.OnPointerExitAsObservable().Subscribe(_ => DragSession.UnregisterTarget());
|
||||
}
|
||||
|
||||
private static bool IsTextInputFocused()
|
||||
{
|
||||
return EventSystem.current != null &&
|
||||
EventSystem.current.currentSelectedGameObject != null &&
|
||||
EventSystem.current.currentSelectedGameObject.GetComponent<TMP_InputField>() != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 055f68d70b7c7634991717c468672f15
|
||||
35
Assets/Scripts/Gameplay/AetheriaUnityGameplayLoopShell.cs
Normal file
35
Assets/Scripts/Gameplay/AetheriaUnityGameplayLoopShell.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
|
||||
public sealed class AetheriaUnityGameplayLoopShell
|
||||
{
|
||||
public bool Paused { get; set; }
|
||||
public Func<Entity> ResolveCurrentEntity { get; set; }
|
||||
public Func<bool> IsCurrentEntityUndocked { get; set; }
|
||||
public Action ApplyLatestZoneRender { get; set; }
|
||||
public AetheriaUnityCurrentEntityPresentation CurrentEntityPresentation { get; set; }
|
||||
public AetheriaUnityPilotFrameAdapter PilotFrameAdapter { get; set; }
|
||||
public AetheriaUnityTargetPresentation TargetPresentation { get; set; }
|
||||
|
||||
public void Tick(float deltaTime, float time)
|
||||
{
|
||||
if (Paused)
|
||||
return;
|
||||
|
||||
ApplyLatestZoneRender?.Invoke();
|
||||
CurrentEntityPresentation?.Tick(deltaTime);
|
||||
if (IsCurrentEntityUndocked?.Invoke() == true)
|
||||
PilotFrameAdapter?.Tick(ResolveCurrentEntity?.Invoke(), deltaTime, time);
|
||||
}
|
||||
|
||||
public void LateTick()
|
||||
{
|
||||
TargetPresentation?.UpdateTargetIndicators(
|
||||
ResolveCurrentEntity?.Invoke(),
|
||||
IsCurrentEntityUndocked?.Invoke() == true,
|
||||
CurrentEntityPresentation);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f4705e6dd36d4d89add3f3751008bf82
|
||||
147
Assets/Scripts/Gameplay/AetheriaUnityGameplaySceneWiring.cs
Normal file
147
Assets/Scripts/Gameplay/AetheriaUnityGameplaySceneWiring.cs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using Cinemachine;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering.PostProcessing;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public sealed class AetheriaUnityGameplaySceneWiring
|
||||
{
|
||||
public ZoneRenderer ZoneRenderer { get; set; }
|
||||
public CinemachineVirtualCamera DockCamera { get; set; }
|
||||
public CinemachineVirtualCamera FollowCamera { get; set; }
|
||||
public MenuPanel Menu { get; set; }
|
||||
public TradeMenu TradeMenu { get; set; }
|
||||
public CanvasGroup GameplayUI { get; set; }
|
||||
public InventoryMenu Inventory { get; set; }
|
||||
public InventoryPanel ShipPanel { get; set; }
|
||||
public InventoryPanel TargetShipPanel { get; set; }
|
||||
public SchematicDisplay SchematicDisplay { get; set; }
|
||||
public SchematicDisplay TargetSchematicDisplay { get; set; }
|
||||
public PlaceUIElementWorldspace[] Crosshairs { get; set; } = Array.Empty<PlaceUIElementWorldspace>();
|
||||
public Prototype LockIndicator { get; set; }
|
||||
public GameObject HitMarker { get; set; }
|
||||
public float HitMarkerDuration { get; set; }
|
||||
public Image TargetShieldsBackground { get; set; }
|
||||
public Image TargetShieldsIcon { get; set; }
|
||||
public Color ShieldColor { get; set; }
|
||||
public Color NoShieldColor { get; set; }
|
||||
public Sprite ShieldIcon { get; set; }
|
||||
public Sprite NoShieldIcon { get; set; }
|
||||
public Prototype HostileTargetIndicator { get; set; }
|
||||
public Prototype FriendlyTargetIndicator { get; set; }
|
||||
public PlaceUIElementWorldspace ViewDot { get; set; }
|
||||
public PlaceUIElementWorldspace TargetIndicator { get; set; }
|
||||
public Image TargetHitpointsFill { get; set; }
|
||||
public Image TargetVisibilityFill { get; set; }
|
||||
public Image VisibilityToTargetFill { get; set; }
|
||||
public Image TargetShieldsFill { get; set; }
|
||||
public MainMenu MainMenu { get; set; }
|
||||
|
||||
public void ConfigureCurrentEntityPresentation(
|
||||
AetheriaUnityCurrentEntityPresentation presentation,
|
||||
AetheriaRuntimeCatalogSnapshot runtimeCatalog)
|
||||
{
|
||||
if (presentation == null)
|
||||
return;
|
||||
|
||||
presentation.ZoneRenderer = ZoneRenderer;
|
||||
presentation.RuntimeCatalog = runtimeCatalog;
|
||||
presentation.DockCamera = DockCamera;
|
||||
presentation.FollowCamera = FollowCamera;
|
||||
presentation.Menu = Menu;
|
||||
presentation.TradeMenu = TradeMenu;
|
||||
presentation.GameplayUI = GameplayUI;
|
||||
presentation.ShipPanel = ShipPanel;
|
||||
presentation.TargetShipPanel = TargetShipPanel;
|
||||
presentation.SchematicDisplay = SchematicDisplay;
|
||||
presentation.TargetSchematicDisplay = TargetSchematicDisplay;
|
||||
presentation.Crosshairs = Crosshairs;
|
||||
presentation.LockIndicator = LockIndicator;
|
||||
presentation.HitMarker = HitMarker;
|
||||
presentation.HitMarkerDuration = HitMarkerDuration;
|
||||
presentation.TargetImages = new ImageTargetPresentation
|
||||
{
|
||||
TargetShieldsBackground = TargetShieldsBackground,
|
||||
TargetShieldsIcon = TargetShieldsIcon,
|
||||
ShieldColor = ShieldColor,
|
||||
NoShieldColor = NoShieldColor,
|
||||
ShieldIcon = ShieldIcon,
|
||||
NoShieldIcon = NoShieldIcon
|
||||
};
|
||||
}
|
||||
|
||||
public void ConfigureTargetPresentation(
|
||||
AetheriaUnityTargetPresentation presentation,
|
||||
AetheriaRuntimeCatalogSnapshot runtimeCatalog,
|
||||
AetheriaUnityObservedFacadeIndex observedFacadeIndex,
|
||||
AetheriaUnityObservedTargetQuery observedTargetQuery,
|
||||
Func<AetheriaClient> resolveClient)
|
||||
{
|
||||
if (presentation == null)
|
||||
return;
|
||||
|
||||
presentation.ZoneRenderer = ZoneRenderer;
|
||||
presentation.RuntimeCatalog = runtimeCatalog;
|
||||
presentation.HostileTargetIndicator = HostileTargetIndicator;
|
||||
presentation.FriendlyTargetIndicator = FriendlyTargetIndicator;
|
||||
presentation.ViewDot = ViewDot;
|
||||
presentation.TargetIndicator = TargetIndicator;
|
||||
presentation.TargetHitpointsFill = TargetHitpointsFill;
|
||||
presentation.TargetVisibilityFill = TargetVisibilityFill;
|
||||
presentation.VisibilityToTargetFill = VisibilityToTargetFill;
|
||||
presentation.TargetShieldsFill = TargetShieldsFill;
|
||||
presentation.ResolveClient = resolveClient;
|
||||
presentation.ResolveEntity = daemonEntityIndex =>
|
||||
observedFacadeIndex != null &&
|
||||
observedFacadeIndex.TryResolveEntityByDaemonIndex(daemonEntityIndex, out var entity)
|
||||
? entity
|
||||
: null;
|
||||
presentation.ResolveTarget = observedTargetQuery.GetObservedTarget;
|
||||
presentation.ResolveInfoGathered = observedTargetQuery.GetObservedInfoGathered;
|
||||
presentation.ResolveHostileContact = observedTargetQuery.IsObservedHostileContact;
|
||||
presentation.ResolveVisibleContacts = observedTargetQuery.GetObservedVisibleContacts;
|
||||
}
|
||||
|
||||
public void ConfigureInventoryDragSession(AetheriaUnityDragSession dragSession)
|
||||
{
|
||||
Inventory?.SetDragSession(dragSession);
|
||||
ShipPanel?.SetDragSession(dragSession);
|
||||
TargetShipPanel?.SetDragSession(dragSession);
|
||||
}
|
||||
|
||||
public void ConfigureActionBarPresentation(
|
||||
AetheriaUnityActionBarPresentation actionBarPresentation,
|
||||
AetheriaUnityGameplayInputShell gameplayInputShell,
|
||||
AetheriaRuntimeCatalogSnapshot runtimeCatalog,
|
||||
GameSettings settings,
|
||||
Func<Entity> resolveCurrentEntity,
|
||||
Func<AetheriaClient> resolveActionBarClient)
|
||||
{
|
||||
actionBarPresentation?.Bind(
|
||||
gameplayInputShell?.ActionBarSlots ?? Array.Empty<ActionBarSlot>(),
|
||||
runtimeCatalog,
|
||||
settings,
|
||||
resolveCurrentEntity,
|
||||
resolveActionBarClient);
|
||||
Inventory?.SetActionBarPresentation(actionBarPresentation);
|
||||
}
|
||||
|
||||
public void ConfigureRuntimeInputScreenShell(AetheriaUnityMenuShell menuShell)
|
||||
{
|
||||
MainMenu?.SetRuntimeInputScreenShell(menuShell.CanOpenRuntimeInputScreen, menuShell.ShowRuntimeInputScreen);
|
||||
}
|
||||
|
||||
public void ConfigureObservedFacadeIndex(AetheriaUnityObservedFacadeIndex observedFacadeIndex)
|
||||
{
|
||||
Inventory?.SetObservedFacadeIndex(observedFacadeIndex);
|
||||
ShipPanel?.SetObservedFacadeIndex(observedFacadeIndex);
|
||||
TargetShipPanel?.SetObservedFacadeIndex(observedFacadeIndex);
|
||||
foreach (var localMenu in UnityEngine.Object.FindObjectsByType<LocalMenu>(FindObjectsSortMode.None))
|
||||
localMenu.SetObservedFacadeIndex(observedFacadeIndex);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9e78937590444255a8c972a3d4f69130
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
63
Assets/Scripts/Gameplay/AetheriaUnityLoadoutItemProjector.cs
Normal file
63
Assets/Scripts/Gameplay/AetheriaUnityLoadoutItemProjector.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
|
||||
public sealed class AetheriaUnityLoadoutItemProjector
|
||||
{
|
||||
private readonly ItemManager _itemManager;
|
||||
private readonly AetheriaRuntimeCatalogSnapshot _catalog;
|
||||
|
||||
public AetheriaUnityLoadoutItemProjector(
|
||||
ItemManager itemManager,
|
||||
AetheriaRuntimeCatalogSnapshot catalog)
|
||||
{
|
||||
_itemManager = itemManager ?? throw new ArgumentNullException(nameof(itemManager));
|
||||
_catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
|
||||
}
|
||||
|
||||
public ItemInstance CreateLoadoutItem(AetheriaRuntimeLoadoutItemCommit item)
|
||||
{
|
||||
if (item == null)
|
||||
return null;
|
||||
|
||||
return CreateLoadoutItem(
|
||||
item.ItemKey,
|
||||
item.Quality,
|
||||
item.Durability,
|
||||
Math.Max(1, item.Quantity));
|
||||
}
|
||||
|
||||
public ItemInstance CreateLoadoutItem(AetheriaRuntimeLoadoutItemSnapshot item)
|
||||
{
|
||||
if (item == null)
|
||||
return null;
|
||||
|
||||
return CreateLoadoutItem(
|
||||
item.ItemKey,
|
||||
item.Quality,
|
||||
item.Durability,
|
||||
Math.Max(1, item.Quantity));
|
||||
}
|
||||
|
||||
private ItemInstance CreateLoadoutItem(
|
||||
string itemKey,
|
||||
double quality,
|
||||
double durability,
|
||||
int quantity)
|
||||
{
|
||||
var typedItem = _catalog.FindItem(itemKey ?? "");
|
||||
if (typedItem == null)
|
||||
return null;
|
||||
|
||||
if (typedItem.Stackable)
|
||||
return _itemManager.CreateSimpleCommodityInstance(typedItem, Math.Max(1, quantity));
|
||||
|
||||
var instance = _itemManager.CreateCraftedInstance(typedItem, (float)quality);
|
||||
if (instance is EquippableItem equippable && durability > 0)
|
||||
equippable.Durability = (float)durability;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f14a61accda77214798bd71817fa2dcb
|
||||
129
Assets/Scripts/Gameplay/AetheriaUnityMenuShell.cs
Normal file
129
Assets/Scripts/Gameplay/AetheriaUnityMenuShell.cs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
public sealed class AetheriaUnityMenuShell
|
||||
{
|
||||
public MainMenu MainMenu { get; set; }
|
||||
public GameObject HelpScreen { get; set; }
|
||||
public InputDisplayLayout InputDisplayLayout { get; set; }
|
||||
public GameObject UiRoot { get; set; }
|
||||
public MenuPanel Menu { get; set; }
|
||||
public CanvasGroup GameplayUI { get; set; }
|
||||
public Func<Entity> ResolveCurrentEntity { get; set; }
|
||||
public Func<bool> IsCurrentEntityUndocked { get; set; }
|
||||
public Func<Entity, Entity> ResolveObservedTarget { get; set; }
|
||||
public Action<bool> SetPaused { get; set; }
|
||||
public Action EnablePlayerInput { get; set; }
|
||||
public Action DisablePlayerInput { get; set; }
|
||||
public Action UpdatePlayerPanel { get; set; }
|
||||
public Action<Entity> UpdateTargetPanel { get; set; }
|
||||
|
||||
private bool _menuShownBeforeFullscreen;
|
||||
|
||||
public bool CanOpenRuntimeInputScreen()
|
||||
{
|
||||
return ResolveCurrentEntity?.Invoke() != null &&
|
||||
HelpScreen != null &&
|
||||
InputDisplayLayout != null;
|
||||
}
|
||||
|
||||
public void ShowRuntimeInputScreen()
|
||||
{
|
||||
ShowFullscreenMenu(HelpScreen);
|
||||
}
|
||||
|
||||
public void ToggleFullscreenMenu(GameObject menu)
|
||||
{
|
||||
if (IsTextInputFocused() || menu == null || ResolveCurrentEntity?.Invoke() == null)
|
||||
return;
|
||||
|
||||
if (menu.activeSelf)
|
||||
HideFullscreenMenu(menu);
|
||||
else
|
||||
ShowFullscreenMenu(menu);
|
||||
}
|
||||
|
||||
public void ToggleMenuTab(MenuTab tab)
|
||||
{
|
||||
if (IsTextInputFocused() || MainMenu == null || Menu == null)
|
||||
return;
|
||||
if (MainMenu.gameObject.activeSelf)
|
||||
return;
|
||||
|
||||
if (Menu.gameObject.activeSelf && Menu.CurrentTab == tab)
|
||||
{
|
||||
Menu.gameObject.SetActive(false);
|
||||
if (IsCurrentEntityUndocked?.Invoke() == true)
|
||||
{
|
||||
EnablePlayerInput?.Invoke();
|
||||
RefreshCurrentPanels();
|
||||
if (GameplayUI != null)
|
||||
GameplayUI.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DisablePlayerInput?.Invoke();
|
||||
Menu.ShowTab(tab);
|
||||
if (GameplayUI != null)
|
||||
GameplayUI.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void ShowFullscreenMenu(GameObject menu)
|
||||
{
|
||||
if (IsTextInputFocused() || menu == null || ResolveCurrentEntity?.Invoke() == null)
|
||||
return;
|
||||
|
||||
if (MainMenu != null && MainMenu.gameObject != menu)
|
||||
MainMenu.gameObject.SetActive(false);
|
||||
if (HelpScreen != null && HelpScreen != menu)
|
||||
HelpScreen.SetActive(false);
|
||||
|
||||
SetPaused?.Invoke(true);
|
||||
menu.SetActive(true);
|
||||
if (UiRoot != null)
|
||||
UiRoot.SetActive(false);
|
||||
|
||||
_menuShownBeforeFullscreen = Menu != null && Menu.gameObject.activeSelf;
|
||||
if (!_menuShownBeforeFullscreen)
|
||||
DisablePlayerInput?.Invoke();
|
||||
}
|
||||
|
||||
private void HideFullscreenMenu(GameObject menu)
|
||||
{
|
||||
if (menu == null || ResolveCurrentEntity?.Invoke() == null)
|
||||
return;
|
||||
|
||||
SetPaused?.Invoke(false);
|
||||
menu.SetActive(false);
|
||||
if (UiRoot != null)
|
||||
UiRoot.SetActive(true);
|
||||
|
||||
if (!_menuShownBeforeFullscreen)
|
||||
{
|
||||
EnablePlayerInput?.Invoke();
|
||||
RefreshCurrentPanels();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCurrentPanels()
|
||||
{
|
||||
var currentEntity = ResolveCurrentEntity?.Invoke();
|
||||
UpdatePlayerPanel?.Invoke();
|
||||
UpdateTargetPanel?.Invoke(ResolveObservedTarget?.Invoke(currentEntity));
|
||||
}
|
||||
|
||||
private static bool IsTextInputFocused()
|
||||
{
|
||||
return EventSystem.current != null &&
|
||||
EventSystem.current.currentSelectedGameObject != null &&
|
||||
EventSystem.current.currentSelectedGameObject.GetComponent<TMP_InputField>() != null;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Gameplay/AetheriaUnityMenuShell.cs.meta
Normal file
2
Assets/Scripts/Gameplay/AetheriaUnityMenuShell.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1a95f06d9ca2ddb47a07fdc47791f5af
|
||||
106
Assets/Scripts/Gameplay/AetheriaUnityObservedDockingIndex.cs
Normal file
106
Assets/Scripts/Gameplay/AetheriaUnityObservedDockingIndex.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
|
||||
public sealed class AetheriaUnityObservedDockingIndex
|
||||
{
|
||||
private readonly Func<AetheriaClient> _resolveClient;
|
||||
private readonly AetheriaUnityObservedFacadeIndex _observedFacadeIndex;
|
||||
|
||||
public AetheriaUnityObservedDockingIndex(
|
||||
Func<AetheriaClient> resolveClient,
|
||||
AetheriaUnityObservedFacadeIndex observedFacadeIndex)
|
||||
{
|
||||
_resolveClient = resolveClient ?? throw new ArgumentNullException(nameof(resolveClient));
|
||||
_observedFacadeIndex = observedFacadeIndex ?? throw new ArgumentNullException(nameof(observedFacadeIndex));
|
||||
}
|
||||
|
||||
public int ResolveEntityZoneIndex(Entity entity)
|
||||
{
|
||||
return TryResolveCurrentDocking(entity, out var docking)
|
||||
? docking.ZoneIndex
|
||||
: -1;
|
||||
}
|
||||
|
||||
public bool IsEntityUndocked(Entity entity)
|
||||
{
|
||||
return TryResolveCurrentDocking(entity, out var docking) &&
|
||||
!docking.IsDocked;
|
||||
}
|
||||
|
||||
public bool TryResolveDockingBay(
|
||||
Entity child,
|
||||
out Entity dockParent,
|
||||
out EquippedDockingBay dockingBay)
|
||||
{
|
||||
return TryResolveDockingBay(child, out dockParent, out dockingBay, out _);
|
||||
}
|
||||
|
||||
public bool TryResolveDockingBay(
|
||||
Entity child,
|
||||
out Entity dockParent,
|
||||
out EquippedDockingBay dockingBay,
|
||||
out AetheriaRuntimeCurrentDockingDocument docking)
|
||||
{
|
||||
dockParent = null;
|
||||
dockingBay = null;
|
||||
docking = null;
|
||||
if (!TryResolveCurrentDocking(child, out docking) || !docking.IsDocked)
|
||||
return false;
|
||||
|
||||
if (!_observedFacadeIndex.TryResolveEntityByRecordKey(docking.DockParentEntityKey, out var parent) ||
|
||||
!(parent is OrbitalEntity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parent.DockingBays == null ||
|
||||
docking.DockingBayIndex < 0 ||
|
||||
docking.DockingBayIndex >= parent.DockingBays.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dockParent = parent;
|
||||
dockingBay = parent.DockingBays[docking.DockingBayIndex];
|
||||
return dockingBay != null;
|
||||
}
|
||||
|
||||
private bool TryResolveCurrentDocking(
|
||||
Entity entity,
|
||||
out AetheriaRuntimeCurrentDockingDocument docking)
|
||||
{
|
||||
docking = ReadCurrentDocking();
|
||||
if (entity == null ||
|
||||
docking == null ||
|
||||
docking.CurrentEntityIndex != entity.DaemonEntityIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_observedFacadeIndex.TryResolveEntityRecordKey(entity, out var entityKey) &&
|
||||
!string.IsNullOrWhiteSpace(docking.CurrentEntityKey) &&
|
||||
!string.Equals(docking.CurrentEntityKey, entityKey, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private AetheriaRuntimeCurrentDockingDocument ReadCurrentDocking()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _resolveClient()?.CurrentDockingAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3131bc989a39393489b141a3d040e186
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue