Discover browser world surfaces through Odin
This commit is contained in:
parent
1eb0ef4470
commit
6270999e11
9 changed files with 137 additions and 95 deletions
|
|
@ -182,8 +182,10 @@ Start the browser reference surface runner:
|
|||
powershell -ExecutionPolicy Bypass -File .\scripts\start-browser-reference.ps1
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:8891/`. The page can render local fixture and
|
||||
advertisement surfaces:
|
||||
Open `http://127.0.0.1:8798/`. Odin-owned Hermodr serves the browser reference
|
||||
and lowers Odin-visible provider advertisements and surfaces. Local fixtures
|
||||
remain an offline oracle when the live catalog is unavailable. The page can
|
||||
render:
|
||||
|
||||
- `Fensalir Direct2D`, a fixture surface for the planned
|
||||
CultMesh-to-`AquariumUiDocument` lowering.
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ advertisements and typed command/receipt documents are the boundary.
|
|||
attachment, export consumer, and report artifacts.
|
||||
- Eve retains the browser reference as the behavior oracle and a minimal generic
|
||||
interactive-world fixture.
|
||||
- The browser reference starts through Odin-owned Hermodr, preserves advertised
|
||||
provider and surface identity, selects `interactive-world` by capability, and
|
||||
resolves assets through provider-advertised CDN routes. Its live startup path
|
||||
contains no Aetheria identifiers; local catalogs remain offline oracle data.
|
||||
|
||||
## Remaining Work
|
||||
|
||||
|
|
@ -127,9 +131,9 @@ advertisements and typed command/receipt documents are the boundary.
|
|||
|
||||
### Generic Game Lowering
|
||||
|
||||
- Route browser and remaining runtimes through Odin's aggregated provider
|
||||
discovery rather than local catalogs or sibling paths; Unity's direct
|
||||
CultMesh Verse discovery is the working reference.
|
||||
- Route remaining runtimes through Odin's aggregated provider discovery rather
|
||||
than local catalogs or sibling paths; browser/Hermodr and Unity direct Verse
|
||||
discovery are the working references.
|
||||
- Expand world lowering only through generic surface contracts: entities,
|
||||
transforms, animation state, collision/navigation intent, cameras, effects,
|
||||
interaction prompts, and command boundaries.
|
||||
|
|
|
|||
10
packages/eve-browser-lowering/dist/index.js
vendored
10
packages/eve-browser-lowering/dist/index.js
vendored
|
|
@ -836,7 +836,9 @@ async function resolveGravitySurfaceDocuments(node, props, options, state) {
|
|||
async function fetchGravitySurfaceDocument(request, options) {
|
||||
if (typeof fetch !== "function" || typeof window === "undefined")
|
||||
return undefined;
|
||||
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "aetheria";
|
||||
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "";
|
||||
if (!providerId)
|
||||
return undefined;
|
||||
const params = new URLSearchParams();
|
||||
params.set("documentId", request.documentId);
|
||||
if (request.schemaId)
|
||||
|
|
@ -1525,7 +1527,7 @@ function gravityBodiesFromDocument(document) {
|
|||
waveSpeed: numberProp(body.waveSpeed, 0),
|
||||
icon: assetUriFromRef(body.iconAsset) || assetUriFromRef(displayBody?.iconAsset),
|
||||
iconSize: numberProp(displayBody?.iconSize, numberProp(body.iconSize, 0)),
|
||||
tint: "cultmesh://aetheria/assets/textures/tint_splat",
|
||||
tint: "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -2224,10 +2226,6 @@ function textClassName(kind, props, node) {
|
|||
classes.push("detail", "mono");
|
||||
if (classes.length === 0)
|
||||
classes.push("detail", "eve-text");
|
||||
if (node.id === "aetheria.main_menu.root.title")
|
||||
classes.push("eve-root-title");
|
||||
if (node.id === "aetheria.main_menu.root.subtitle")
|
||||
classes.push("eve-root-subtitle");
|
||||
return classes.join(" ");
|
||||
}
|
||||
function applyControlBoxProps(element, props) {
|
||||
|
|
|
|||
|
|
@ -1017,7 +1017,8 @@ async function fetchGravitySurfaceDocument(
|
|||
options: EveBrowserLoweringOptions,
|
||||
): Promise<unknown> {
|
||||
if (typeof fetch !== "function" || typeof window === "undefined") return undefined;
|
||||
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "aetheria";
|
||||
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "";
|
||||
if (!providerId) return undefined;
|
||||
const params = new URLSearchParams();
|
||||
params.set("documentId", request.documentId);
|
||||
if (request.schemaId) params.set("schemaId", request.schemaId);
|
||||
|
|
@ -1864,7 +1865,7 @@ function gravityBodiesFromDocument(document: Record<string, unknown> | undefined
|
|||
waveSpeed: numberProp(body.waveSpeed, 0),
|
||||
icon: assetUriFromRef((body as Record<string, unknown>).iconAsset) || assetUriFromRef(displayBody?.iconAsset),
|
||||
iconSize: numberProp(displayBody?.iconSize, numberProp((body as Record<string, unknown>).iconSize, 0)),
|
||||
tint: "cultmesh://aetheria/assets/textures/tint_splat",
|
||||
tint: "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -2642,8 +2643,6 @@ function textClassName(kind: string, props: Record<string, unknown>, node: EveSu
|
|||
if (kind === "label") classes.push("cultui-label");
|
||||
if (props.role === "mono") classes.push("detail", "mono");
|
||||
if (classes.length === 0) classes.push("detail", "eve-text");
|
||||
if (node.id === "aetheria.main_menu.root.title") classes.push("eve-root-title");
|
||||
if (node.id === "aetheria.main_menu.root.subtitle") classes.push("eve-root-subtitle");
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +1,28 @@
|
|||
param(
|
||||
[int] $Port = 8891
|
||||
[int] $Port = 8798,
|
||||
[string] $OdinRoot = "E:\Projects\Odin",
|
||||
[string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" })
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$webRoot = Join-Path $projectRoot "web"
|
||||
$logRoot = Join-Path $projectRoot "artifacts\web"
|
||||
New-Item -ItemType Directory -Force $logRoot | Out-Null
|
||||
|
||||
$pidPath = Join-Path $logRoot "eve-browser-reference.pid"
|
||||
$outLog = Join-Path $logRoot "eve-browser-reference.out.log"
|
||||
$errLog = Join-Path $logRoot "eve-browser-reference.err.log"
|
||||
|
||||
if (Test-Path $pidPath) {
|
||||
$oldPid = Get-Content $pidPath -ErrorAction SilentlyContinue
|
||||
if ($oldPid) {
|
||||
Stop-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue
|
||||
}
|
||||
$launcher = Join-Path $OdinRoot "scripts\start-hermodr.ps1"
|
||||
if (-not (Test-Path -LiteralPath $launcher)) {
|
||||
throw "Odin-owned Hermodr launcher is missing: $launcher"
|
||||
}
|
||||
|
||||
$nodeScript = @'
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const root = process.argv[2];
|
||||
const port = Number(process.argv[3]);
|
||||
const types = {'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.mjs':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json; charset=utf-8','.eve':'text/plain; charset=utf-8'};
|
||||
http.createServer((req, res) => {
|
||||
const urlPath = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
|
||||
const clean = urlPath === '/' ? '/index.html' : urlPath;
|
||||
const file = path.normalize(path.join(root, clean));
|
||||
if (!file.startsWith(root)) {
|
||||
res.writeHead(403);
|
||||
res.end('forbidden');
|
||||
return;
|
||||
$healthUrl = "http://127.0.0.1:$Port/health"
|
||||
try {
|
||||
$health = Invoke-RestMethod -Uri $healthUrl -TimeoutSec 2
|
||||
if ($health.ok -and $health.service -eq "hermodr-browser-lowering") {
|
||||
Write-Host "Eve browser reference already available at http://127.0.0.1:$Port/"
|
||||
exit 0
|
||||
}
|
||||
fs.readFile(file, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end('not found');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, {'content-type': types[path.extname(file)] || 'application/octet-stream'});
|
||||
res.end(data);
|
||||
});
|
||||
}).listen(port, '127.0.0.1', () => console.log(`Eve browser reference listening on http://127.0.0.1:${port}`));
|
||||
'@
|
||||
} catch {
|
||||
# Hermodr is not running on the requested local port.
|
||||
}
|
||||
|
||||
$scriptPath = Join-Path $logRoot "serve-eve-reference.cjs"
|
||||
Set-Content -LiteralPath $scriptPath -Value $nodeScript -Encoding UTF8
|
||||
$proc = Start-Process -FilePath "node" -ArgumentList @($scriptPath, $webRoot, $Port) -WorkingDirectory $projectRoot -RedirectStandardOutput $outLog -RedirectStandardError $errLog -PassThru -WindowStyle Hidden
|
||||
$proc.Id | Set-Content -LiteralPath $pidPath
|
||||
Start-Sleep -Milliseconds 500
|
||||
Get-Content $outLog -Tail 5
|
||||
Write-Host "PID $($proc.Id)"
|
||||
& $launcher -HostAddress "127.0.0.1" -Port $Port -OdinCultMeshUri $OdinCultMeshUri -NoWindow
|
||||
if (-not $?) {
|
||||
throw "Hermodr failed to start."
|
||||
}
|
||||
Write-Host "Eve browser reference available at http://127.0.0.1:$Port/"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
"supportedFeatures": [
|
||||
"embeddedDocuments",
|
||||
"providerAdvertisements",
|
||||
"odinProviderDiscovery",
|
||||
"genericWorldDiscovery",
|
||||
"providerAssets",
|
||||
"stateBindings",
|
||||
"styleTokens",
|
||||
"authorityWitnesses"
|
||||
|
|
@ -57,6 +60,8 @@
|
|||
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
|
||||
"evidencePaths": [
|
||||
"web/surface.js",
|
||||
"web/hermodr-provider-catalog.mjs",
|
||||
"scripts/start-browser-reference.ps1",
|
||||
"packages/eve-browser-lowering/src/index.ts",
|
||||
"web/fixtures/eve-world-smoke.provider-advertisement.json"
|
||||
]
|
||||
|
|
@ -79,10 +84,11 @@
|
|||
"test": {
|
||||
"ownerRepo": "Eve",
|
||||
"status": "node-tests-and-parity-harness",
|
||||
"command": "node --test web/eve-dsl.test.mjs && npm --prefix packages/eve-browser-lowering test && powershell -ExecutionPolicy Bypass -File .\\scripts\\run-parity-harness.ps1",
|
||||
"command": "node --test web/*.test.mjs && npm --prefix packages/eve-browser-lowering test && powershell -ExecutionPolicy Bypass -File .\\scripts\\run-parity-harness.ps1",
|
||||
"evidencePaths": [
|
||||
"web/eve-dsl.test.mjs",
|
||||
"web/provider-advertisements.test.mjs",
|
||||
"web/hermodr-provider-catalog.test.mjs",
|
||||
"packages/eve-browser-lowering/test/browser-lowering.test.mjs",
|
||||
"../EveConformance/tools/parity/parity-manifest.json",
|
||||
"scripts/run-parity-harness.ps1"
|
||||
|
|
|
|||
41
web/hermodr-provider-catalog.mjs
Normal file
41
web/hermodr-provider-catalog.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
export function createHermodrProviderTargets(catalog) {
|
||||
const providers = new Map((catalog?.providers || [])
|
||||
.map(provider => [provider.id || provider.providerId, provider])
|
||||
.filter(([providerId]) => providerId));
|
||||
|
||||
return (catalog?.surfaces || [])
|
||||
.filter(surface => surface?.providerId && surface?.surfaceId)
|
||||
.map(surface => {
|
||||
const provider = providers.get(surface.providerId) || {};
|
||||
const advertisedSurface = (provider.surfaces || [])
|
||||
.find(candidate => candidate.surfaceId === surface.surfaceId) || {};
|
||||
const surfaceId = surface.surfaceId;
|
||||
const surfaceKind = surface.surfaceKind || advertisedSurface.surfaceKind || "";
|
||||
return {
|
||||
...provider,
|
||||
providerId: surface.providerId,
|
||||
targetId: `${surface.providerId}::${surfaceId}`,
|
||||
title: surface.title || advertisedSurface.title || provider.title || surfaceId,
|
||||
kind: provider.kind || "eve.provider",
|
||||
freshness: { state: "odin-visible-cultmesh" },
|
||||
surfaces: [{
|
||||
...advertisedSurface,
|
||||
transport: "hermodr-surface",
|
||||
surfaceId,
|
||||
surfaceKind,
|
||||
url: `/hermodr/surface/${encodeURIComponent(surface.providerId)}?surfaceId=${encodeURIComponent(surfaceId)}`,
|
||||
}],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function selectInitialProviderTarget(targets, requestedId = "", preferredSurfaceKind = "interactive-world") {
|
||||
const available = Array.isArray(targets) ? targets : [];
|
||||
return available.find(target =>
|
||||
target.targetId === requestedId ||
|
||||
target.providerId === requestedId ||
|
||||
target.surfaces?.some(surface => surface.surfaceId === requestedId) ||
|
||||
target.aliases?.includes(requestedId))
|
||||
|| available.find(target => target.surfaces?.some(surface => surface.surfaceKind === preferredSurfaceKind))
|
||||
|| available[0];
|
||||
}
|
||||
38
web/hermodr-provider-catalog.test.mjs
Normal file
38
web/hermodr-provider-catalog.test.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
createHermodrProviderTargets,
|
||||
selectInitialProviderTarget,
|
||||
} from "./hermodr-provider-catalog.mjs";
|
||||
|
||||
const catalog = {
|
||||
providers: [{
|
||||
id: "fixture.game",
|
||||
title: "Fixture game",
|
||||
surfaces: [
|
||||
{ surfaceId: "fixture.menu", surfaceKind: "menu" },
|
||||
{ surfaceId: "fixture.world", surfaceKind: "interactive-world" },
|
||||
],
|
||||
}],
|
||||
surfaces: [
|
||||
{ providerId: "fixture.game", surfaceId: "fixture.menu", title: "Menu" },
|
||||
{ providerId: "fixture.game", surfaceId: "fixture.world", surfaceKind: "interactive-world", title: "World" },
|
||||
],
|
||||
};
|
||||
|
||||
test("preserves provider and advertised surface identity from Hermodr", () => {
|
||||
const targets = createHermodrProviderTargets(catalog);
|
||||
assert.equal(targets.length, 2);
|
||||
assert.equal(targets[1].providerId, "fixture.game");
|
||||
assert.equal(targets[1].targetId, "fixture.game::fixture.world");
|
||||
assert.equal(targets[1].surfaces[0].surfaceId, "fixture.world");
|
||||
assert.equal(targets[1].surfaces[0].surfaceKind, "interactive-world");
|
||||
assert.match(targets[1].surfaces[0].url, /surfaceId=fixture\.world/);
|
||||
});
|
||||
|
||||
test("selects an interactive world without product identity", () => {
|
||||
const targets = createHermodrProviderTargets(catalog);
|
||||
assert.equal(selectInitialProviderTarget(targets).targetId, "fixture.game::fixture.world");
|
||||
assert.equal(selectInitialProviderTarget(targets, "fixture.menu").targetId, "fixture.game::fixture.menu");
|
||||
});
|
||||
|
|
@ -5,6 +5,10 @@ import {
|
|||
} from "../packages/eve-browser-lowering/dist/index.js";
|
||||
import { compileEveDsl } from "./eve-dsl.js";
|
||||
import { mergeProviderAdvertisement } from "./provider-advertisements.mjs";
|
||||
import {
|
||||
createHermodrProviderTargets,
|
||||
selectInitialProviderTarget,
|
||||
} from "./hermodr-provider-catalog.mjs";
|
||||
|
||||
const statusEl = document.querySelector("#status");
|
||||
const app = document.querySelector("#app");
|
||||
|
|
@ -19,7 +23,7 @@ let liveHermodr = false;
|
|||
let openProviderGeneration = 0;
|
||||
|
||||
providerSelect.addEventListener("change", () => {
|
||||
const provider = providers.find(candidate => candidate.providerId === providerSelect.value);
|
||||
const provider = providers.find(candidate => (candidate.targetId || candidate.providerId) === providerSelect.value);
|
||||
if (provider) void openProvider(provider);
|
||||
});
|
||||
|
||||
|
|
@ -34,18 +38,16 @@ async function bootProviders() {
|
|||
}
|
||||
providerSelect.replaceChildren(...providers.map(provider => {
|
||||
const option = document.createElement("option");
|
||||
option.value = provider.providerId;
|
||||
option.value = provider.targetId || provider.providerId;
|
||||
option.textContent = provider.title || provider.providerId;
|
||||
return option;
|
||||
}));
|
||||
const requestedProviderId = new URLSearchParams(location.search).get("provider");
|
||||
const firstProduct = providers.find(provider => provider.providerId === requestedProviderId || provider.aliases?.includes(requestedProviderId))
|
||||
|| providers.find(provider => provider.providerId === "aetheria")
|
||||
|| providers.find(provider => provider.providerId === "aetheria.main_menu.root")
|
||||
|| providers.find(provider => provider.providerId === "aetheria.inventory.panel")
|
||||
|| providers.find(provider => provider.providerId === "repixelizer")
|
||||
|| providers[0];
|
||||
providerSelect.value = firstProduct.providerId;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const requestedProviderId = params.get("surface") || params.get("provider") || "";
|
||||
const firstProduct = liveHermodr
|
||||
? selectInitialProviderTarget(providers, requestedProviderId)
|
||||
: providers.find(provider => provider.providerId === requestedProviderId || provider.aliases?.includes(requestedProviderId)) || providers[0];
|
||||
providerSelect.value = firstProduct.targetId || firstProduct.providerId;
|
||||
await openProvider(firstProduct);
|
||||
}
|
||||
|
||||
|
|
@ -60,27 +62,7 @@ async function loadHermodrProviders() {
|
|||
const response = await fetch("/hermodr/verse/catalog", { cache: "no-store" });
|
||||
if (!response.ok) return [];
|
||||
const catalog = await response.json();
|
||||
const advertised = new Map();
|
||||
for (const provider of catalog.providers || []) {
|
||||
advertised.set(provider.id || provider.providerId, provider);
|
||||
}
|
||||
return (catalog.surfaces || [])
|
||||
.filter(surface => surface?.providerId && surface?.surface)
|
||||
.map(surface => {
|
||||
const provider = advertised.get(surface.providerId) || {};
|
||||
return {
|
||||
...provider,
|
||||
providerId: surface.providerId,
|
||||
title: surface.title || provider.title || surface.providerId,
|
||||
kind: provider.kind || "eve.surface",
|
||||
freshness: { state: "odin-visible-cultmesh" },
|
||||
surfaces: [{
|
||||
transport: "hermodr-surface",
|
||||
surfaceId: surface.providerId,
|
||||
url: `/hermodr/surface/${encodeURIComponent(surface.providerId)}`,
|
||||
}],
|
||||
};
|
||||
});
|
||||
return createHermodrProviderTargets(catalog);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue