feat: expand portal registry schema (#718)
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
This commit was merged in pull request #718.
This commit is contained in:
@@ -102,22 +102,47 @@ A portal is a game configuration. To add one:
|
|||||||
"name": "New Game",
|
"name": "New Game",
|
||||||
"description": "What this portal is.",
|
"description": "What this portal is.",
|
||||||
"status": "offline",
|
"status": "offline",
|
||||||
|
"portal_type": "game-world",
|
||||||
|
"world_category": "rpg",
|
||||||
|
"environment": "staging",
|
||||||
|
"access_mode": "operator",
|
||||||
|
"readiness_state": "prototype",
|
||||||
|
"telemetry_source": "hermes-harness:new-game-bridge",
|
||||||
|
"owner": "Timmy",
|
||||||
"app_id": 12345,
|
"app_id": 12345,
|
||||||
"window_title": "New Game Window Title",
|
"window_title": "New Game Window Title",
|
||||||
"destination": {
|
"destination": {
|
||||||
"type": "harness",
|
"type": "harness",
|
||||||
|
"action_label": "Enter New Game",
|
||||||
"params": { "world": "new-world" }
|
"params": { "world": "new-world" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **No code changes.** The heartbeat loop reads `portals.json`,
|
Required metadata fields:
|
||||||
uses `app_id` for Steam API calls and `window_title` for
|
- `portal_type` — high-level kind (`game-world`, `operator-room`, `research-space`, `experiment`)
|
||||||
screenshot targeting. The MCP tools are game-agnostic.
|
- `world_category` — subtype for navigation and grouping (`rpg`, `workspace`, `sim`, etc.)
|
||||||
|
- `environment` — `production`, `staging`, or `local`
|
||||||
|
- `access_mode` — `public`, `operator`, or `local-only`
|
||||||
|
- `readiness_state` — `playable`, `active`, `prototype`, `rebuilding`, `blocked`, `offline`
|
||||||
|
- `telemetry_source` — where truth/status comes from
|
||||||
|
- `owner` — who currently owns the world or integration lane
|
||||||
|
- `destination.action_label` — human-facing action text for UI cards/directories
|
||||||
|
|
||||||
|
2. **No mandatory game-specific code changes.** The heartbeat loop reads `portals.json`,
|
||||||
|
uses metadata for grouping/status/visibility, and can still use fields like
|
||||||
|
`app_id` and `window_title` for screenshot targeting where relevant. The MCP tools remain game-agnostic.
|
||||||
|
|
||||||
3. **Game-specific prompts** go in `training/data/prompts_*.yaml`
|
3. **Game-specific prompts** go in `training/data/prompts_*.yaml`
|
||||||
to teach the model what the game looks like and how to play it.
|
to teach the model what the game looks like and how to play it.
|
||||||
|
|
||||||
|
4. **Migration from legacy portal definitions**
|
||||||
|
- old portal entries with only `id`, `name`, `description`, `status`, and `destination`
|
||||||
|
should be upgraded in place
|
||||||
|
- preserve visual fields like `color`, `position`, and `rotation`
|
||||||
|
- add the new metadata fields so the same registry can drive future atlas, status wall,
|
||||||
|
preview cards, and many-portal navigation without inventing parallel registries
|
||||||
|
|
||||||
## Portal: Bannerlord (Primary)
|
## Portal: Bannerlord (Primary)
|
||||||
|
|
||||||
**Steam App ID:** `261550`
|
**Steam App ID:** `261550`
|
||||||
|
|||||||
26
portals.json
26
portals.json
@@ -4,12 +4,20 @@
|
|||||||
"name": "Morrowind",
|
"name": "Morrowind",
|
||||||
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
|
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
|
||||||
"status": "online",
|
"status": "online",
|
||||||
|
"portal_type": "game-world",
|
||||||
|
"world_category": "rpg",
|
||||||
|
"environment": "production",
|
||||||
|
"access_mode": "public",
|
||||||
|
"readiness_state": "playable",
|
||||||
|
"telemetry_source": "hermes-harness:morrowind-mcp",
|
||||||
|
"owner": "Timmy",
|
||||||
"color": "#ff6600",
|
"color": "#ff6600",
|
||||||
"position": { "x": 15, "y": 0, "z": -10 },
|
"position": { "x": 15, "y": 0, "z": -10 },
|
||||||
"rotation": { "y": -0.5 },
|
"rotation": { "y": -0.5 },
|
||||||
"destination": {
|
"destination": {
|
||||||
"url": "https://morrowind.timmy.foundation",
|
"url": "https://morrowind.timmy.foundation",
|
||||||
"type": "harness",
|
"type": "harness",
|
||||||
|
"action_label": "Enter Vvardenfell",
|
||||||
"params": { "world": "vvardenfell" }
|
"params": { "world": "vvardenfell" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -17,13 +25,21 @@
|
|||||||
"id": "bannerlord",
|
"id": "bannerlord",
|
||||||
"name": "Bannerlord",
|
"name": "Bannerlord",
|
||||||
"description": "Calradia battle harness. Massive armies, tactical command.",
|
"description": "Calradia battle harness. Massive armies, tactical command.",
|
||||||
"status": "online",
|
"status": "rebuilding",
|
||||||
|
"portal_type": "game-world",
|
||||||
|
"world_category": "strategy-rpg",
|
||||||
|
"environment": "staging",
|
||||||
|
"access_mode": "operator",
|
||||||
|
"readiness_state": "prototype",
|
||||||
|
"telemetry_source": "hermes-harness:bannerlord-bridge",
|
||||||
|
"owner": "Timmy",
|
||||||
"color": "#ffd700",
|
"color": "#ffd700",
|
||||||
"position": { "x": -15, "y": 0, "z": -10 },
|
"position": { "x": -15, "y": 0, "z": -10 },
|
||||||
"rotation": { "y": 0.5 },
|
"rotation": { "y": 0.5 },
|
||||||
"destination": {
|
"destination": {
|
||||||
"url": "https://bannerlord.timmy.foundation",
|
"url": "https://bannerlord.timmy.foundation",
|
||||||
"type": "harness",
|
"type": "harness",
|
||||||
|
"action_label": "Enter Calradia",
|
||||||
"params": { "world": "calradia" }
|
"params": { "world": "calradia" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -32,12 +48,20 @@
|
|||||||
"name": "Workshop",
|
"name": "Workshop",
|
||||||
"description": "The creative harness. Build, script, and manifest.",
|
"description": "The creative harness. Build, script, and manifest.",
|
||||||
"status": "online",
|
"status": "online",
|
||||||
|
"portal_type": "operator-room",
|
||||||
|
"world_category": "workspace",
|
||||||
|
"environment": "local",
|
||||||
|
"access_mode": "local-only",
|
||||||
|
"readiness_state": "active",
|
||||||
|
"telemetry_source": "hermes-harness:workshop-state",
|
||||||
|
"owner": "Alexander",
|
||||||
"color": "#4af0c0",
|
"color": "#4af0c0",
|
||||||
"position": { "x": 0, "y": 0, "z": -20 },
|
"position": { "x": 0, "y": 0, "z": -20 },
|
||||||
"rotation": { "y": 0 },
|
"rotation": { "y": 0 },
|
||||||
"destination": {
|
"destination": {
|
||||||
"url": "https://workshop.timmy.foundation",
|
"url": "https://workshop.timmy.foundation",
|
||||||
"type": "harness",
|
"type": "harness",
|
||||||
|
"action_label": "Enter Workshop",
|
||||||
"params": { "mode": "creative" }
|
"params": { "mode": "creative" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
tests/test_portal_registry_schema.py
Normal file
45
tests/test_portal_registry_schema.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
REQUIRED_TOP_LEVEL_KEYS = {
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"status",
|
||||||
|
"portal_type",
|
||||||
|
"world_category",
|
||||||
|
"environment",
|
||||||
|
"access_mode",
|
||||||
|
"readiness_state",
|
||||||
|
"telemetry_source",
|
||||||
|
"owner",
|
||||||
|
"destination",
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRED_DESTINATION_KEYS = {"type", "action_label"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_portals_json_uses_expanded_registry_schema() -> None:
|
||||||
|
portals = json.loads(Path("portals.json").read_text())
|
||||||
|
|
||||||
|
assert portals, "portals.json should define at least one portal"
|
||||||
|
for portal in portals:
|
||||||
|
assert REQUIRED_TOP_LEVEL_KEYS.issubset(portal.keys())
|
||||||
|
assert REQUIRED_DESTINATION_KEYS.issubset(portal["destination"].keys())
|
||||||
|
|
||||||
|
|
||||||
|
def test_gameportal_protocol_documents_new_metadata_fields_and_migration() -> None:
|
||||||
|
protocol = Path("GAMEPORTAL_PROTOCOL.md").read_text()
|
||||||
|
|
||||||
|
for term in [
|
||||||
|
"portal_type",
|
||||||
|
"world_category",
|
||||||
|
"environment",
|
||||||
|
"access_mode",
|
||||||
|
"readiness_state",
|
||||||
|
"telemetry_source",
|
||||||
|
"owner",
|
||||||
|
"Migration from legacy portal definitions",
|
||||||
|
]:
|
||||||
|
assert term in protocol
|
||||||
Reference in New Issue
Block a user