From 608d49c60651221f405cb6896066750bb5c16649 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:19:49 -0400 Subject: [PATCH 01/11] Propose finite paid chamber scope and sequential interlock acceptance --- ...chamber-sequential-interlocks-milestone.md | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/development/paid-chamber-sequential-interlocks-milestone.md diff --git a/docs/development/paid-chamber-sequential-interlocks-milestone.md b/docs/development/paid-chamber-sequential-interlocks-milestone.md new file mode 100644 index 00000000..6c207e9d --- /dev/null +++ b/docs/development/paid-chamber-sequential-interlocks-milestone.md @@ -0,0 +1,164 @@ +# Paid two-door chamber: implementation proposal + +Status: design only, awaiting Coordinator scope review before runtime/schema edits. +Base: `1f2e5a71d0a2a9b6c1ac56ac0c1354ea2b200d58`, following PR148 merge +`25293641ab9263f699768d7d585a42cd80a2ab2d`. +Exclusive implementation owner: chat `01a16aa0-d610-7e41-b5a4-0868e3742b28`. +Coordinator `01a1283c-45d6-7e61-bedd-905e8797f03d` owns scope review, exact-head +NO-PORT runtime leases and final CI acceptance/merge. Worktree `paid-chamber-successor`, +branch `paid-chamber-sequential-interlocks`. One implementation chat; no delegation. + +## Observable outcome and bounds + +A fresh playable world can acquire finite ore, refine and print its own materials, +fabricate and hold a real crowbar, build one 3.6 m3 chamber with two paid manual +doors, fit two seals/two recoverable bypasses and a distinct paid 6 kg containment +kit, then commission and deliberately operate it through hosted player controls. +The chamber is mutually exclusive with the existing commissioned one-door room. +Its inner endpoint is explicitly `habitat_main`; outer is `exterior_reservoir`. +These are logical connections, without spatial atmosphere mapping or implied EVA, +breathability, species, pumps, temperature/species mixing or general networks. + +Retain the geometry and independent source analysis in +[the prerequisite design](paid-two-door-chamber-milestone.md#full-successor-target-geometry-and-budget): +four walls, doors at [5050,0,-4000]/[6950,0,-4000] quarter-turn1, interior +x[5100,6900] z[-4500,-3500] y[0,2000], floor x[4100,7000]. Floor and ceiling +are tiled into existing legal panel sizes, never oversized single panels. Actual +topology, PhysicsServer contact, occupancy and clearance must establish usability. +Retain area 1.71 m2 and crowbar 1000 N. Explicit prototype containment tuning is +110 kPa, supported only by existing Access actuator integrity/seal condition and +validated paid geometry/kit links; no invented wall-health model or certification. + +## Rechecked finite acquisition budget + +At this base `data/production/catalog.v3.json` contains 10+7+7 = 24 kg ore. +`refine.iron` yields 0.6 kg iron/0.4 kg tails per 1 kg input for 1000 J; +`craft.plate` consumes 1 kg iron and 1000 J. Production starts at 100000 J, +charges 250 J per extraction and caps depot mass at 100 kg. The existing +fabrication path costs 2 kg iron and 2000 J for the held crowbar. + +| Bill | Mass / energy | +| --- | ---: | +| Four walls / two doors | 4 / 4 kg plate | +| Floor including swings / ceiling | 2.9 / 1.8 kg plate | +| Two seals / two bypasses | 0.4 / 0.4 kg plate | +| Distinct containment kit | 6 kg plate | +| Installed plate | 19.5 kg | +| 37 ore extractions | 9250 J | +| Refine 37 kg ore | 22.2 kg iron + 14.8 kg tails; 37000 J | +| Print 20 kg plate / fabricate crowbar | 20000 / 2000 J | +| Total / remaining electrical energy | 68250 / 31750 J | + +Final accounted matter: 19.5 kg installed + 2 kg crowbar + 0.5 kg plate + +0.2 kg iron + 14.8 kg tails = 37 kg. Untouched 5 kg ice/10 kg regolith make +52 kg world origin. One-kg extraction/unload batches fit the 2 kg carrier; +even all acquired material fits depot capacity. Existing conservative wear over +37 completed cycles leaves tools usable. Manual assembly/recovery and passive +gas transfer cost zero electricity; no new power, tool or stock grant. + +Propose catalog v4: preserve v1/v2/v3 bytes and add a distinct reachable +`deposit.iron.chamber` with exactly 13000000 mg ore, explicit fingerprint, +source position/reach and selectable UI label. New-world composition opts into +v4. Legacy catalogs, missing-Production migration and depleted sources receive +no added source or refill. Legacy players may only build from their own remaining +stock; this milestone does not promise the full route in existing v3 worlds. +The 6 kg kit is separately paid, with an immutable receipt. The earlier 4 kg +room kit must be recovered normally before reusing its material; it cannot +be interpreted as a chamber kit or pay for a free second door. + +## Owners, admission, ticking and recovery + +Production owns geometry, kit/seal/bypass materials, finite origin, reservation +claims and exactly-once debit/refund receipts. Access owns both actuator states, +seal condition, collision-safe opening/closing and the unchanged force check. +Pressure owns one separate chamber record, volume, sensors, endpoints and valve +states. Inventory/control owns body, session, held usable tool and reach. UI +owns selection/pending presentation only. Extend existing prepared-owner +transactions and whole-world detached validation, rather than a second authority. + +Commission only a complete owned closed intact pair with both bypasses closed, +no existing commissioned room, supported geometry and paid kit. Subdivide 3.6 m3 +from the current 1000 m3 habitat proportionally at its actual temperature/moles; +allocate nothing from an infinite reservoir. Decommission only with both doors +closed, both valves closed, intact geometry and both boundaries within the manual +pressure margin; merge chamber gas/volume back into habitat conservatively. +Decommission itself neither moves the body nor opens geometry. Recover the kit +exactly once after decommission with reservation-aware receiving capacity; then +allow ordinary paid-part recovery subject to current occupant/support guards. +Recommission retained hardware explicitly without healing condition. + +Commands use explicit desired state, current Production revision and restore +epoch, authenticated session/body/owner and held crowbar/reach. Check both sides +again at apply, including low-level Access admission. Opposite door and bypass +must be closed/intact before opening a selected bypass or door; own bypass must +be closed before door operation. Opening both valves, both doors or a +door-plus-bypass is rejected. Closing remains available when safe for occupancy. +Request replay returns the same receipt; changed intent with reused ID rejects. + +Outbound: close inner, open outer bypass, wait for actual force-safe differential, +close bypass, open outer, pass through, close outer. Return: outer closed, inner +bypass open until finite pair equilibrium permits force, close bypass, open inner, +pass through, close inner. No automatic valve sequencing or target-pressure writes. +Damaged opposite boundary leaks and blocks unsafe admission; UI cannot advertise +isolation. Blocked transitions retain gas, stock, receipts and actuator state. + +At the fixed pressure tick, retain existing prototype transfers, then process +chamber inner followed by outer before sensors. Per boundary use max(passive +leak, open bypass) at existing conductance 1 mol/(s*kPa), not additive paths. +Open/degraded/overrated boundaries leak; closed valves never suppress leakage. +Use existing equal-temperature equilibrium-capped solver, nonnegative moles and +0.001 kPa epsilon. Persist ordering; bounded transfer/leak/block counters only. +Finite exterior and habitat change each cycle. Compare all-volume total moles +and volume at every lifecycle step; the unrelated prototype may also consume +these reservoirs. No constant101.325kPa refill assumption. + +## Save, observation and player control contract + +Propose Production save15, observation17, paid-door6 and Pressure save5, with +explicit chamber/kit v1 records. Keep existing room dictionaries and decoders +semantically unchanged. Preserve Access version unless its exact contract needs +new fields; enforce cross-owner pair rules through detached validation. New +chamber records require unique paired IDs, fixed endpoint identities, exact bills, +matching owner/body/world/region/geometry/area, sensor/volume links and valve +states. Validate full Production/Pressure/Access/Inventory candidates before any +assignment. Malformed half-restores must leave every owner, tick, receipt, +revision and epoch unchanged. No permissive union or repair-on-restore. + +Extend the existing collapsed paid-door controls with explicit chamber pair, +inner/outer selection, kit cost/fit/recover and commission/decommission actions. +Show three actual pressures, selected delta/load, opposite boundary state, +both valves, pending receipt and exact blocking reason. Actions enqueue existing +authenticated host commands; stale/disconnected/rebound state disables them +until a fresh authority observation. No client pressure writes or sequencing. +Exercise actual source selector, action queue, projection and control paths. + +## Acceptance and verification budget + +Positive: unseeded fresh first-playable acquire37kg/refine/print/fabricate/build +route, exact68250J and all mass accounted; actual body support/contact through +both apertures; both manual cycle directions; habitat isolated during outer vent; +finite pressure loss over repeated cycles; all-volume mole/volume conservation; +kit recover/refit and whole-world reload with open/closed valve resume. + +Negative controls: insufficient/reserved stock, carrier/depot/refund capacity, +power/tool/reach, foreign owner/body/session, stale revision/epoch, replay and +changed intent, malformed paired restore with all-owner equality, false bill/ +endpoint/kit/area/rating, missing/orphan links, unsupported/overlapping sweeps, +occupied closure, both-open doors/valves and door-plus-bypass, damaged opposite +seal/actuator, both pressure signs exceeding1000N, overrated leak, closed valve +not suppressing leak, unchanged old0.5kPa policy, depleted/exhausted source reload, +legacy no-grant and recovery with no healing/refill. Preserve finite bootstrap, +room/bypass/reinforcement, UI, snapshot and emitted-schema regressions. + +Bound work to this one usable slice and its documentation/schemas/tests. No new +agent chats, generalized networks, repairs, art, dependencies or env variables. +Static design/arithmetic/diff review first; runtime/schema work starts only after +Coordinator accepts this pinned scope. Then smallest meaningful authored checks, +freeze/push exact head, request one bounded exclusive NO-PORT lease with named +stages and time budget. Use versioned AST-extracted Invoke-GodotCheck, empty +preflight inventory, stop on first diagnostic, preserve logs, do not edit/retry +under lease, postflight inventory and explicit release. Required Windows/Linux +CI and emitted-schema negatives must pass on the final head; distinguish tested +merge revision. Capture only if material usability uncertainty remains after real +control/runtime checks. Final task-closeout and matching-head Coordinator review +precede merge. No deployment, publication, media, financial or outreach action. From 734b63029d7841046e0e2b9700e498a2264f114d Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:21:53 -0400 Subject: [PATCH 02/11] Record Coordinator acceptance and conservative recovery constraints --- .../paid-chamber-sequential-interlocks-milestone.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/development/paid-chamber-sequential-interlocks-milestone.md b/docs/development/paid-chamber-sequential-interlocks-milestone.md index 6c207e9d..6c109726 100644 --- a/docs/development/paid-chamber-sequential-interlocks-milestone.md +++ b/docs/development/paid-chamber-sequential-interlocks-milestone.md @@ -1,6 +1,7 @@ # Paid two-door chamber: implementation proposal -Status: design only, awaiting Coordinator scope review before runtime/schema edits. +Status: Coordinator accepted design `608d49c60651221f405cb6896066750bb5c16649` +for bounded implementation on 2026-10-23. Runtime/schema implementation pending. Base: `1f2e5a71d0a2a9b6c1ac56ac0c1354ea2b200d58`, following PR148 merge `25293641ab9263f699768d7d585a42cd80a2ab2d`. Exclusive implementation owner: chat `01a16aa0-d610-7e41-b5a4-0868e3742b28`. @@ -82,6 +83,8 @@ from the current 1000 m3 habitat proportionally at its actual temperature/moles; allocate nothing from an infinite reservoir. Decommission only with both doors closed, both valves closed, intact geometry and both boundaries within the manual pressure margin; merge chamber gas/volume back into habitat conservatively. +The UI must explain that this may require depressurizing the finite habitat; +routine lossless dismantling beside vacuum is not available in this slice. Decommission itself neither moves the body nor opens geometry. Recover the kit exactly once after decommission with reservation-aware receiving capacity; then allow ordinary paid-part recovery subject to current occupant/support guards. @@ -92,7 +95,8 @@ epoch, authenticated session/body/owner and held crowbar/reach. Check both sides again at apply, including low-level Access admission. Opposite door and bypass must be closed/intact before opening a selected bypass or door; own bypass must be closed before door operation. Opening both valves, both doors or a -door-plus-bypass is rejected. Closing remains available when safe for occupancy. +door-plus-bypass is rejected. Opposite hardware damage must never block a safe +closing action; own actuator and occupancy rules still apply. Request replay returns the same receipt; changed intent with reused ID rejects. Outbound: close inner, open outer bypass, wait for actual force-safe differential, From a9f4b3c42993f3750fee2edc80df180dc6eaef7c Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:46:36 -0400 Subject: [PATCH 03/11] Implement paid finite two-door chamber and sequential interlocks --- .../production-recipe-observations.yml | 11 +- data/production/catalog.v4.json | 133 ++ docs/architecture/production-gameplay-loop.md | 38 + ...chamber-sequential-interlocks-milestone.md | 59 +- .../paid-door-observation-v6.schema.json | 251 ++++ docs/schema/pressure-system-v5.schema.json | 289 ++++ docs/schema/production-intent-v1.schema.json | 178 ++- .../production-observation-v17.schema.json | 960 +++++++++++++ docs/schema/production-system-v15.schema.json | 1208 +++++++++++++++++ docs/schema/world-snapshot-v1.schema.json | 6 +- .../network/door_contact_observation.gd | 15 +- .../network/remote_production_projection.gd | 8 +- src/app/first_playable_bootstrap.gd | 3 +- src/core/access/access_control_system.gd | 14 + src/core/access/prepared_door_change.gd | 38 +- src/core/pressure/pressure_room_owner.gd | 4 + src/core/pressure/pressure_system.gd | 143 +- src/core/production/paid_chamber.gd | 167 +++ src/core/production/paid_door_construction.gd | 17 +- src/core/production/paid_room_bypass.gd | 16 +- .../production/paid_room_reinforcement.gd | 2 +- .../production_simulation_system.gd | 115 +- src/core/runtime/world_runtime.gd | 3 +- .../habitat/procedural_habitat_preview.gd | 12 +- .../production/paid_door_controls.gd | 17 +- .../commissioned_room_equalization_test.gd | 4 +- tests/godot/paid_chamber_test.gd | 394 ++++++ tests/godot/paid_room_reinforcement_test.gd | 6 +- tests/schema/paid_chamber_schema_test.py | 72 + 29 files changed, 4085 insertions(+), 98 deletions(-) create mode 100644 data/production/catalog.v4.json create mode 100644 docs/schema/paid-door-observation-v6.schema.json create mode 100644 docs/schema/pressure-system-v5.schema.json create mode 100644 docs/schema/production-observation-v17.schema.json create mode 100644 docs/schema/production-system-v15.schema.json create mode 100644 src/core/production/paid_chamber.gd create mode 100644 tests/godot/paid_chamber_test.gd create mode 100644 tests/schema/paid_chamber_schema_test.py diff --git a/.github/workflows/production-recipe-observations.yml b/.github/workflows/production-recipe-observations.yml index 84b46792..a7ba37ed 100644 --- a/.github/workflows/production-recipe-observations.yml +++ b/.github/workflows/production-recipe-observations.yml @@ -23,6 +23,12 @@ on: - 'tests/schema/finite_bootstrap*' - 'data/production/catalog.v2.json' - 'data/production/catalog.v3.json' + - 'data/production/catalog.v4.json' + - 'src/app/first_playable_bootstrap.gd' + - 'src/core/access/access_control_system.gd' + - 'src/presentation/habitat/procedural_habitat_preview.gd' + - 'tests/godot/paid_chamber*' + - 'tests/schema/paid_chamber*' - 'data/production/manual-room-reinforcement*' - 'tests/godot/paid_room_reinforcement*' - 'tests/schema/room_reinforcement*' @@ -86,7 +92,7 @@ jobs: include-templates: false - name: Run bounded recipe, resource and maintenance checks shell: pwsh - run: ./tools/ci/run-godot-checks.ps1 -TestNames 'recipe_requirements_test.gd','workstation_resource_view_test.gd','repair_usability_test.gd','first_playable_task_guide_test.gd','machinery_runtime_test.gd','checkpoint_recovery_test.gd','timed_canister_controls_test.gd','paid_horizontal_panels_test.gd','paid_door_lifecycle_test.gd','paid_door_pressure_room_test.gd','finite_bootstrap_acquisition_test.gd','commissioned_room_equalization_test.gd','paid_room_reinforcement_test.gd','horizontal_panel_controls_test.gd','horizontal_floor_support_query_test.gd','paid_floor_support_adapter_test.gd','paid_panel_collision_descriptors_test.gd','paid_panel_contact_scene_test.gd' + run: ./tools/ci/run-godot-checks.ps1 -TestNames 'recipe_requirements_test.gd','workstation_resource_view_test.gd','repair_usability_test.gd','first_playable_task_guide_test.gd','machinery_runtime_test.gd','checkpoint_recovery_test.gd','timed_canister_controls_test.gd','paid_horizontal_panels_test.gd','paid_door_lifecycle_test.gd','paid_door_pressure_room_test.gd','finite_bootstrap_acquisition_test.gd','commissioned_room_equalization_test.gd','paid_room_reinforcement_test.gd','paid_chamber_test.gd','horizontal_panel_controls_test.gd','horizontal_floor_support_query_test.gd','paid_floor_support_adapter_test.gd','paid_panel_collision_descriptors_test.gd','paid_panel_contact_scene_test.gd' - name: Validate real emitted JSON against local versioned schemas shell: pwsh run: | @@ -104,6 +110,8 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'Finite bootstrap schema checks failed.' } & $schemaPython tests/schema/paid_door_schema_test.py if ($LASTEXITCODE -ne 0) { throw 'Paid door schema checks failed.' } + & $schemaPython tests/schema/paid_chamber_schema_test.py + if ($LASTEXITCODE -ne 0) { throw 'Paid chamber schemas failed.' } & $schemaPython tests/schema/room_reinforcement_schema_test.py if ($LASTEXITCODE -ne 0) { throw 'Room reinforcement schemas failed.' } & $schemaPython tests/schema/room_equalization_schema_test.py @@ -137,6 +145,7 @@ jobs: var/paid-door-pressure-fixture.json var/room-equalization-fixture.json var/room-reinforcement-fixture.json + var/paid-chamber-fixture.json var/horizontal-panel-controls-fixture.json var/horizontal-floor-support-fixture.json var/paid-floor-locomotion-fixture.json diff --git a/data/production/catalog.v4.json b/data/production/catalog.v4.json new file mode 100644 index 00000000..6a72ddfb --- /dev/null +++ b/data/production/catalog.v4.json @@ -0,0 +1,133 @@ +{ + "schema_version": 1, + "materials": [ + "ore.iron", + "ice", + "regolith", + "iron", + "tailings", + "water", + "hydrogen", + "oxygen", + "salts", + "silicate", + "nutrient-solution", + "plate" + ], + "dispensable": [ + "water", + "hydrogen", + "oxygen", + "nutrient-solution" + ], + "deposits": { + "deposit.iron": { + "material": "ore.iron", + "mass_milligrams": 10000000 + }, + "deposit.ice": { + "material": "ice", + "mass_milligrams": 5000000 + }, + "deposit.regolith": { + "material": "regolith", + "mass_milligrams": 10000000 + }, + "deposit.iron.outcrop": { + "material": "ore.iron", + "mass_milligrams": 7000000 + }, + "deposit.iron.reinforcement": { + "material": "ore.iron", + "mass_milligrams": 7000000 + }, + "deposit.iron.chamber": { + "material": "ore.iron", + "mass_milligrams": 15000000 + } + }, + "recipes": { + "refine.iron": { + "station": "refinery", + "inputs": { + "ore.iron": 1000000 + }, + "outputs": { + "iron": 600000, + "tailings": 400000 + }, + "duration_ticks": 120, + "energy_joules": 1000 + }, + "refine.ice": { + "station": "refinery", + "inputs": { + "ice": 1000000 + }, + "outputs": { + "water": 1000000 + }, + "duration_ticks": 60, + "energy_joules": 500 + }, + "refine.regolith": { + "station": "refinery", + "inputs": { + "regolith": 1000000 + }, + "outputs": { + "salts": 200000, + "silicate": 400000, + "tailings": 400000 + }, + "duration_ticks": 120, + "energy_joules": 1000 + }, + "chemical.electrolysis": { + "station": "chemical", + "inputs": { + "water": 1000000 + }, + "outputs": { + "hydrogen": 111000, + "oxygen": 889000 + }, + "duration_ticks": 180, + "energy_joules": 2000 + }, + "chemical.nutrients": { + "station": "chemical", + "inputs": { + "water": 900000, + "salts": 100000 + }, + "outputs": { + "nutrient-solution": 1000000 + }, + "duration_ticks": 120, + "energy_joules": 500 + }, + "craft.plate": { + "station": "printer", + "inputs": { + "iron": 1000000 + }, + "outputs": { + "plate": 1000000 + }, + "duration_ticks": 120, + "energy_joules": 1000 + }, + "salvage.plate": { + "station": "recycler", + "inputs": { + "plate": 1000000 + }, + "outputs": { + "iron": 1000000 + }, + "duration_ticks": 60, + "energy_joules": 500 + } + } +} diff --git a/docs/architecture/production-gameplay-loop.md b/docs/architecture/production-gameplay-loop.md index b8a9fbf0..71f89921 100644 --- a/docs/architecture/production-gameplay-loop.md +++ b/docs/architecture/production-gameplay-loop.md @@ -284,3 +284,41 @@ Local UI request IDs include a random runtime-session prefix and a monotonic sequence. Saved production receipts retain their original IDs for idempotent retry; creating a runtime to reload a snapshot cannot restart those identities. The prefix is transport identity only and grants no simulation authority. + +The default first-playable factory now selects catalog4 explicitly. Its distinct +15 kg chamber iron seam extends the previous 24 kg ore origin to 39 kg; unchanged +ice/regolith make 54 kg total origin. Catalog1/2/3 saves and explicit old factory +modes receive no seam grant. The complete two-door route prints 21 kg plate, +installs 20.4 kg, fabricates the 2 kg iron crowbar and spends 71,750 J. It leaves +0.6 kg plate, 0.4 kg iron, 15.6 kg tailings and 28,250 J. + +`PaidChamber` owns synchronous pair validation and detached transaction planning; +Production owns its 6 kg paid kit and geometry. Access owns both original sealed +manual actuators. PreparedDoorChange validates both device comparisons before +the first write. Pressure owns a 3.6 m3 finite allocation split from habitat_main, +its sensor and two manual valve states. Each simulation tick exchanges inner +then outer with finite habitat_main/exterior_reservoir at conductance1, using +one path when leakage and an open valve coincide. This is logical prototype +pressure service, with no spatial atmosphere or EVA capability claim. + +Opening either boundary requires its own valve closed and the opposite boundary +closed, intact and valve closed. Closing retains only its own force/occupancy +guards so an unsafe opposite cannot prevent isolation. Both pressure signs use +the unchanged 1000 N crowbar limit over 1.71 m2. Closed intact kit boundaries +retain up to 110 kPa; seal/actuator damage or overrating leaks even with valves +closed. No extra health field or automatic repair is introduced. Decommissioning +requires both boundaries force-safe and both valves closed; ordinary vacuum +cycling does not supply this precondition. Recovery/refit preserves original +seals, actuator damage and finite gas without refill. + +Production15/observation17/paid-door6 and Pressure5/chamber1 persist the explicit +pair, bills, endpoints, geometry and sensor/volume links. Whole-world restore +validates detached Production/Pressure/Access/Inventory candidates before any +owner assignment; half pairs, orphan links and incompatible open states reject. +The collapsed paid-door controls show the pair, three pressures, opposite state, +valves and force load, and enqueue authenticated commands with current revision +and epoch. Rejections retain owner state and expose the host reason. Existing +bounded simulation counters remain available; no per-frame diagnostic tracing +or external service is added. See the +[chamber milestone](../development/paid-chamber-sequential-interlocks-milestone.md) +for pinned geometry, accepted physical corrections and verification scope. diff --git a/docs/development/paid-chamber-sequential-interlocks-milestone.md b/docs/development/paid-chamber-sequential-interlocks-milestone.md index 6c109726..a68ef359 100644 --- a/docs/development/paid-chamber-sequential-interlocks-milestone.md +++ b/docs/development/paid-chamber-sequential-interlocks-milestone.md @@ -23,7 +23,7 @@ breathability, species, pumps, temperature/species mixing or general networks. Retain the geometry and independent source analysis in [the prerequisite design](paid-two-door-chamber-milestone.md#full-successor-target-geometry-and-budget): four walls, doors at [5050,0,-4000]/[6950,0,-4000] quarter-turn1, interior -x[5100,6900] z[-4500,-3500] y[0,2000], floor x[4100,7000]. Floor and ceiling +x[5100,6900] z[-4500,-3500] y[0,2000], floor x[4100,7900]. Floor and ceiling are tiled into existing legal panel sizes, never oversized single panels. Actual topology, PhysicsServer contact, occupancy and clearance must establish usability. Retain area 1.71 m2 and crowbar 1000 N. Explicit prototype containment tuning is @@ -41,24 +41,24 @@ fabrication path costs 2 kg iron and 2000 J for the held crowbar. | Bill | Mass / energy | | --- | ---: | | Four walls / two doors | 4 / 4 kg plate | -| Floor including swings / ceiling | 2.9 / 1.8 kg plate | +| Floor including swings / ceiling | 3.8 / 1.8 kg plate | | Two seals / two bypasses | 0.4 / 0.4 kg plate | | Distinct containment kit | 6 kg plate | -| Installed plate | 19.5 kg | -| 37 ore extractions | 9250 J | -| Refine 37 kg ore | 22.2 kg iron + 14.8 kg tails; 37000 J | -| Print 20 kg plate / fabricate crowbar | 20000 / 2000 J | -| Total / remaining electrical energy | 68250 / 31750 J | - -Final accounted matter: 19.5 kg installed + 2 kg crowbar + 0.5 kg plate + -0.2 kg iron + 14.8 kg tails = 37 kg. Untouched 5 kg ice/10 kg regolith make -52 kg world origin. One-kg extraction/unload batches fit the 2 kg carrier; +| Installed plate | 20.4 kg | +| 39 ore extractions | 9750 J | +| Refine 39 kg ore | 23.4 kg iron + 15.6 kg tails; 39000 J | +| Print 21 kg plate / fabricate crowbar | 21000 / 2000 J | +| Total / remaining electrical energy | 71750 / 28250 J | + +Final accounted matter: 20.4 kg installed + 2 kg crowbar + 0.6 kg plate + +0.4 kg iron + 15.6 kg tails = 39 kg. Untouched 5 kg ice/10 kg regolith make +54 kg world origin. One-kg extraction/unload batches fit the 2 kg carrier; even all acquired material fits depot capacity. Existing conservative wear over -37 completed cycles leaves tools usable. Manual assembly/recovery and passive +39 completed cycles leaves tools usable. Manual assembly/recovery and passive gas transfer cost zero electricity; no new power, tool or stock grant. Propose catalog v4: preserve v1/v2/v3 bytes and add a distinct reachable -`deposit.iron.chamber` with exactly 13000000 mg ore, explicit fingerprint, +`deposit.iron.chamber` with exactly 15000000 mg ore, explicit fingerprint, source position/reach and selectable UI label. New-world composition opts into v4. Legacy catalogs, missing-Production migration and depleted sources receive no added source or refill. Legacy players may only build from their own remaining @@ -138,8 +138,8 @@ Exercise actual source selector, action queue, projection and control paths. ## Acceptance and verification budget -Positive: unseeded fresh first-playable acquire37kg/refine/print/fabricate/build -route, exact68250J and all mass accounted; actual body support/contact through +Positive: unseeded fresh first-playable acquire39kg/refine/print/fabricate/build +route, exact71750J and all mass accounted; actual body support/contact through both apertures; both manual cycle directions; habitat isolated during outer vent; finite pressure loss over repeated cycles; all-volume mole/volume conservation; kit recover/refit and whole-world reload with open/closed valve resume. @@ -166,3 +166,32 @@ CI and emitted-schema negatives must pass on the final head; distinguish tested merge revision. Capture only if material usability uncertainty remains after real control/runtime checks. Final task-closeout and matching-head Coordinator review precede merge. No deployment, publication, media, financial or outreach action. + +## Accepted support correction + +Coordinator accepted the support correction on 2026-10-23: the original x7000 +floor end leaves only50mm beyond the outer aperture atx6950 and fails the +existing600mm full-footprint support contract. Extend the paid floor tox7900 +using one legal900mm strip. The39kg/71750J/54kg-origin budget above supersedes +the earlier37kg/68250J/52kg proposal. Keep a negative control for the old short +floor and prove full body crossing onto both paid approaches. + +## Accepted handle and preview corrections + +Coordinator accepted two further physical integration corrections on 2026-10-23. +Paid doors use a fixed server-owned handle 900 mm above the floor anchor. Reach +is the existing 1500 mm limit measured from the authorized canonical body center +(`body_center` normalizes legacy y0 and actual capsule y900). All Inventory +authorization, voluntary action and available-hand checks remain mandatory. +Construction, chamber pair operations and low-level opening admission use this +same helper; no client handle or global Inventory reach change is introduced. +The safe inside position x5550 can operate the outer handle at x6950 without +occupying either sweep. Boundary controls cover y0/y900 parity and 1501 mm denial. + +Production focus hides and disables only the explicitly owned negative-side +legacy `DoorFrameWall` mesh/collider crossing the mapped inner aperture. Ordinary +preview keeps it. Surface, HabitatFloor, positive-side wall, prototype functional +doors, logical pressure and all paid geometry retain their existing ownership. +The actual LocalHabitatCharacter test begins at the observed factory spawn, +walks supported waypoints, proves a closed paid leaf blocks, crosses both paid +apertures and mirrors reached positions through canonical movement commands. diff --git a/docs/schema/paid-door-observation-v6.schema.json b/docs/schema/paid-door-observation-v6.schema.json new file mode 100644 index 00000000..41c4154e --- /dev/null +++ b/docs/schema/paid-door-observation-v6.schema.json @@ -0,0 +1,251 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:frontier-systems:schema:paid-door-observation:v6", + "title": "Own paid chamber doors v6", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "control_epoch", + "doors", + "can_operate" + ], + "properties": { + "schema_version": { + "const": 6 + }, + "control_epoch": { + "type": "integer", + "minimum": -1, + "maximum": 9007199254740000 + }, + "doors": { + "type": "array", + "maxItems": 16, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "door_id", + "position", + "quarter_turn", + "opened", + "seal_installed", + "seal_condition", + "pressure_room", + "bypass_installed", + "bypass_open", + "reinforcement_installed", + "reinforcement_intact", + "chamber_kit_installed", + "chamber" + ], + "properties": { + "door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "position": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "prefixItems": [ + { + "type": "integer", + "minimum": -100000, + "maximum": 100000, + "multipleOf": 50 + }, + { + "const": 0 + }, + { + "type": "integer", + "minimum": -100000, + "maximum": 100000, + "multipleOf": 50 + } + ], + "items": false + }, + "quarter_turn": { + "enum": [ + 0, + 1 + ] + }, + "opened": { + "type": "boolean" + }, + "seal_installed": { + "type": "boolean" + }, + "seal_condition": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "pressure_room": { + "oneOf": [ + { + "type": "object", + "maxProperties": 0 + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "volume_m3", + "pressure_kpa", + "ambient_pressure_kpa", + "leaking" + ], + "properties": { + "volume_m3": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 999 + }, + "pressure_kpa": { + "type": "number", + "minimum": 0 + }, + "ambient_pressure_kpa": { + "type": "number", + "minimum": 0 + }, + "leaking": { + "type": "boolean" + } + } + } + ] + }, + "bypass_installed": { + "type": "boolean" + }, + "bypass_open": { + "type": "boolean" + }, + "reinforcement_installed": { + "type": "boolean" + }, + "reinforcement_intact": { + "type": "boolean" + }, + "chamber_kit_installed": { + "type": "boolean" + }, + "chamber": { + "oneOf": [ + { + "type": "object", + "maxProperties": 0 + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "inner_door_id", + "outer_door_id", + "side", + "habitat_pressure_kpa", + "chamber_pressure_kpa", + "exterior_pressure_kpa", + "inner_bypass_open", + "outer_bypass_open" + ], + "properties": { + "inner_door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "outer_door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "side": { + "enum": [ + "inner", + "outer" + ] + }, + "habitat_pressure_kpa": { + "type": "number", + "minimum": 0 + }, + "chamber_pressure_kpa": { + "type": "number", + "minimum": 0 + }, + "exterior_pressure_kpa": { + "type": "number", + "minimum": 0 + }, + "inner_bypass_open": { + "type": "boolean" + }, + "outer_bypass_open": { + "type": "boolean" + } + } + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "bypass_open": { + "const": true + } + } + }, + "then": { + "properties": { + "bypass_installed": { + "const": true + }, + "pressure_room": { + "minProperties": 1 + } + } + } + }, + { + "if": { + "properties": { + "reinforcement_intact": { + "const": true + } + } + }, + "then": { + "properties": { + "reinforcement_installed": { + "const": true + }, + "seal_installed": { + "const": true + }, + "seal_condition": { + "const": 1 + } + } + } + } + ] + } + }, + "can_operate": { + "type": "boolean" + } + } +} diff --git a/docs/schema/pressure-system-v5.schema.json b/docs/schema/pressure-system-v5.schema.json new file mode 100644 index 00000000..d9931f0a --- /dev/null +++ b/docs/schema/pressure-system-v5.schema.json @@ -0,0 +1,289 @@ +{ + "type": "object", + "required": [ + "schema_version", + "volumes", + "airlocks", + "sensors", + "total_transferred_moles", + "constructed_rooms", + "room_valves", + "paid_chamber" + ], + "properties": { + "schema_version": { + "const": 5 + }, + "volumes": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/pressureVolume" + } + }, + "airlocks": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/airlock" + } + }, + "sensors": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/pressureSensor" + } + }, + "total_transferred_moles": { + "type": "number", + "minimum": 0 + }, + "constructed_rooms": { + "type": "object", + "maxProperties": 0 + }, + "room_valves": { + "type": "object", + "minProperties": 2, + "maxProperties": 2, + "propertyNames": { + "pattern": "^door-", + "maxLength": 128 + }, + "additionalProperties": { + "type": "boolean" + } + }, + "paid_chamber": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "policy_id", + "volume_id", + "sensor_id", + "principal_id", + "body_id", + "world_id", + "region_id", + "geometry", + "inner_door_id", + "outer_door_id", + "inner_endpoint", + "outer_endpoint" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "policy_id": { + "const": "constructed-chamber.habitat-exterior.prototype.v1" + }, + "volume_id": { + "type": "string", + "pattern": "^paid-chamber-", + "maxLength": 128 + }, + "sensor_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "principal_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "body_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "world_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "region_id": { + "const": "local-habitat" + }, + "geometry": { + "type": "object", + "additionalProperties": false, + "required": [ + "walls", + "panels", + "doors" + ], + "properties": { + "walls": { + "$ref": "urn:frontier-systems:schema:production-system:v11#/properties/walls" + }, + "panels": { + "$ref": "urn:frontier-systems:schema:production-system:v11#/properties/panels" + }, + "doors": { + "$ref": "urn:frontier-systems:schema:production-system:v11#/properties/doors" + } + } + }, + "inner_door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "outer_door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "inner_endpoint": { + "const": "habitat_main" + }, + "outer_endpoint": { + "const": "exterior_reservoir" + } + } + } + }, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:frontier-systems:schema:pressure-system:v5", + "title": "Finite paired paid chamber pressure state v5", + "$defs": { + "pressureVolume": { + "type": "object", + "required": [ + "schema_version", + "volume_id", + "volume_m3", + "temperature_kelvin", + "gas_moles" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "volume_id": { + "type": "string", + "minLength": 1 + }, + "volume_m3": { + "type": "number", + "exclusiveMinimum": 0 + }, + "temperature_kelvin": { + "type": "number", + "exclusiveMinimum": 0 + }, + "gas_moles": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "airlock": { + "type": "object", + "required": [ + "schema_version", + "airlock_id", + "habitat_volume_id", + "chamber_volume_id", + "exterior_volume_id", + "inner_door_open", + "outer_door_open", + "equalization_valve_open", + "vent_valve_open", + "equalization_conductance_mol_per_second_kpa", + "vent_conductance_mol_per_second_kpa" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "airlock_id": { + "type": "string", + "minLength": 1 + }, + "habitat_volume_id": { + "type": "string", + "minLength": 1 + }, + "chamber_volume_id": { + "type": "string", + "minLength": 1 + }, + "exterior_volume_id": { + "type": "string", + "minLength": 1 + }, + "inner_door_open": { + "type": "boolean" + }, + "outer_door_open": { + "type": "boolean" + }, + "equalization_valve_open": { + "type": "boolean" + }, + "vent_valve_open": { + "type": "boolean" + }, + "equalization_conductance_mol_per_second_kpa": { + "type": "number", + "minimum": 0 + }, + "vent_conductance_mol_per_second_kpa": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "pressureSensor": { + "type": "object", + "required": [ + "schema_version", + "sensor_id", + "volume_id", + "pressure_kpa_absolute", + "last_sampled_tick", + "sample_count" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "sensor_id": { + "type": "string", + "minLength": 1 + }, + "volume_id": { + "type": "string", + "minLength": 1 + }, + "pressure_kpa_absolute": { + "type": "number", + "minimum": 0 + }, + "last_sampled_tick": { + "type": "integer", + "minimum": 0 + }, + "sample_count": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + } + } +} diff --git a/docs/schema/production-intent-v1.schema.json b/docs/schema/production-intent-v1.schema.json index 131287aa..e517f1fe 100644 --- a/docs/schema/production-intent-v1.schema.json +++ b/docs/schema/production-intent-v1.schema.json @@ -68,7 +68,11 @@ "door_bypass_close", "door_bypass_recover", "door_reinforcement_install", - "door_reinforcement_recover" + "door_reinforcement_recover", + "door_chamber_kit_install", + "door_chamber_kit_recover", + "door_chamber_commission", + "door_chamber_decommission" ] }, "payload": { @@ -2075,6 +2079,178 @@ } } } + }, + { + "if": { + "properties": { + "operation": { + "const": "door_chamber_kit_install" + } + }, + "required": [ + "operation" + ] + }, + "then": { + "properties": { + "payload": { + "type": "object", + "additionalProperties": false, + "required": [ + "expected_revision", + "control_epoch", + "door_id" + ], + "properties": { + "expected_revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "control_epoch": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + } + } + } + } + } + }, + { + "if": { + "properties": { + "operation": { + "const": "door_chamber_kit_recover" + } + }, + "required": [ + "operation" + ] + }, + "then": { + "properties": { + "payload": { + "type": "object", + "additionalProperties": false, + "required": [ + "expected_revision", + "control_epoch", + "door_id" + ], + "properties": { + "expected_revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "control_epoch": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + } + } + } + } + } + }, + { + "if": { + "properties": { + "operation": { + "const": "door_chamber_commission" + } + }, + "required": [ + "operation" + ] + }, + "then": { + "properties": { + "payload": { + "type": "object", + "additionalProperties": false, + "required": [ + "expected_revision", + "control_epoch", + "door_id" + ], + "properties": { + "expected_revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "control_epoch": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + } + } + } + } + } + }, + { + "if": { + "properties": { + "operation": { + "const": "door_chamber_decommission" + } + }, + "required": [ + "operation" + ] + }, + "then": { + "properties": { + "payload": { + "type": "object", + "additionalProperties": false, + "required": [ + "expected_revision", + "control_epoch", + "door_id" + ], + "properties": { + "expected_revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "control_epoch": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + } + } + } + } + } } ], "$defs": { diff --git a/docs/schema/production-observation-v17.schema.json b/docs/schema/production-observation-v17.schema.json new file mode 100644 index 00000000..6de0a592 --- /dev/null +++ b/docs/schema/production-observation-v17.schema.json @@ -0,0 +1,960 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:frontier-systems:schema:production-observation:v17", + "title": "Own production paid chamber observation v17", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "catalog_revision", + "catalog_fingerprint", + "timebase_hz", + "last_tick", + "state_revision", + "deposits", + "store", + "vessel", + "wall_mass_milligrams", + "energy_joules", + "powered", + "job", + "cargo", + "components", + "recipes", + "dispensable", + "can_operate", + "vessel_pressure_kpa", + "extractable_sources", + "structures", + "blueprints", + "loose_parts", + "tool_extraction", + "machinery", + "walls", + "recipe_requirements", + "timed_canister", + "horizontal_panels", + "paid_doors" + ], + "properties": { + "schema_version": { + "const": 17 + }, + "catalog_revision": { + "enum": [ + 1, + 2, + 3, + 4 + ] + }, + "catalog_fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "timebase_hz": { + "enum": [ + 60, + 72, + 90 + ] + }, + "last_tick": { + "$ref": "#/$defs/revision" + }, + "state_revision": { + "$ref": "#/$defs/revision" + }, + "deposits": {}, + "store": { + "$ref": "#/$defs/stock" + }, + "vessel": { + "$ref": "#/$defs/stock" + }, + "wall_mass_milligrams": { + "type": "integer", + "minimum": 0, + "maximum": 100000000, + "multipleOf": 1000000 + }, + "energy_joules": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "powered": { + "type": "object", + "additionalProperties": false, + "required": [ + "drill", + "refinery", + "chemical", + "printer", + "recycler", + "dispenser", + "construction" + ], + "properties": { + "drill": { + "type": "boolean" + }, + "refinery": { + "type": "boolean" + }, + "chemical": { + "type": "boolean" + }, + "printer": { + "type": "boolean" + }, + "recycler": { + "type": "boolean" + }, + "dispenser": { + "type": "boolean" + }, + "construction": { + "type": "boolean" + } + } + }, + "job": { + "oneOf": [ + { + "type": "object", + "maxProperties": 0 + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "recipe_id", + "deposit_id", + "station", + "inputs", + "outputs", + "remaining_ticks" + ], + "properties": { + "recipe_id": { + "type": "string", + "maxLength": 128 + }, + "deposit_id": { + "type": "string", + "maxLength": 128 + }, + "station": { + "enum": [ + "drill", + "refinery", + "chemical", + "printer", + "recycler", + "dispenser", + "construction" + ] + }, + "inputs": { + "$ref": "#/$defs/stock" + }, + "outputs": { + "$ref": "#/$defs/stock" + }, + "remaining_ticks": { + "type": "integer", + "minimum": 1, + "maximum": 36000 + } + } + } + ] + }, + "cargo": { + "maxProperties": 32, + "propertyNames": { + "minLength": 1, + "maxLength": 128, + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/stock" + }, + "type": "object" + }, + "components": { + "maxItems": 32, + "uniqueItems": true, + "type": "array", + "items": { + "minLength": 1, + "maxLength": 128, + "type": "string" + } + }, + "recipes": { + "maxItems": 32, + "uniqueItems": true, + "type": "array", + "items": { + "minLength": 1, + "maxLength": 128, + "type": "string" + } + }, + "dispensable": { + "maxItems": 32, + "uniqueItems": true, + "type": "array", + "items": { + "minLength": 1, + "maxLength": 128, + "type": "string" + } + }, + "extractable_sources": { + "maxItems": 32, + "uniqueItems": true, + "type": "array", + "items": { + "minLength": 1, + "maxLength": 128, + "type": "string" + } + }, + "can_operate": { + "type": "boolean" + }, + "vessel_pressure_kpa": { + "maximum": 1000, + "minimum": 0, + "type": "number" + }, + "structures": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "object", + "required": [ + "blueprint_id", + "fingerprint", + "materials" + ], + "properties": { + "materials": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "integer", + "minimum": 1, + "maximum": 100000000 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + } + }, + "fingerprint": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + "blueprint_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + } + }, + "additionalProperties": false + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + } + }, + "blueprints": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "object", + "required": [ + "name", + "definition_id", + "definition_revision", + "materials", + "fabrication_energy_joules", + "salvage_energy_joules", + "fingerprint", + "modular" + ], + "properties": { + "materials": { + "type": "object", + "maxProperties": 32, + "minProperties": 1, + "additionalProperties": { + "type": "integer", + "minimum": 1, + "maximum": 100000000 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + } + }, + "fabrication_energy_joules": { + "type": "integer", + "minimum": 1, + "maximum": 100000 + }, + "salvage_energy_joules": { + "type": "integer", + "minimum": 1, + "maximum": 100000 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "definition_revision": { + "const": 1 + }, + "definition_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "fingerprint": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + "modular": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + } + }, + "loose_parts": { + "type": "object", + "additionalProperties": { + "minProperties": 1, + "maxProperties": 32, + "propertyNames": { + "$ref": "#/$defs/id" + }, + "additionalProperties": { + "maximum": 100000000, + "minimum": 1, + "type": "integer" + }, + "type": "object" + }, + "maxProperties": 128, + "propertyNames": { + "$ref": "#/$defs/id" + } + }, + "tool_extraction": { + "oneOf": [ + { + "maxProperties": 0, + "type": "object" + }, + { + "$ref": "urn:frontier-systems:schema:tool-extraction-work:v1" + } + ] + }, + "machinery": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "catalog_fingerprint", + "parts", + "mounts", + "placements", + "links", + "jobs", + "inventory_items", + "cargo" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "catalog_fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "parts": { + "type": "object", + "maxProperties": 128, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind" + ], + "properties": { + "kind": { + "enum": [ + "frame", + "socket", + "power", + "controller", + "printhead", + "refinery", + "extractor", + "solar", + "router", + "expander", + "cable", + "autolathe" + ] + } + } + } + }, + "mounts": { + "type": "object", + "maxProperties": 128, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "placements": { + "type": "object", + "maxProperties": 20, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "position", + "mode", + "enabled", + "energy", + "generation_remainder" + ], + "properties": { + "position": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "type": "integer", + "minimum": -10000000, + "maximum": 10000000 + } + }, + "mode": { + "enum": [ + "freeform", + "surface" + ] + }, + "enabled": { + "type": "boolean" + }, + "energy": { + "type": "integer", + "minimum": 0, + "maximum": 20000 + }, + "generation_remainder": { + "type": "integer", + "minimum": 0, + "maximum": 89 + } + } + } + }, + "links": { + "type": "object", + "maxProperties": 24, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "b": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + } + } + }, + "jobs": { + "type": "object", + "maxProperties": 20, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "recipe", + "progress", + "energy_spent", + "paused" + ], + "properties": { + "recipe": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "progress": { + "type": "integer", + "minimum": 0, + "maximum": 36000 + }, + "energy_spent": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "paused": { + "type": "boolean" + } + } + } + }, + "inventory_items": { + "type": "array", + "maxItems": 512, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "item_id", + "definition_id", + "definition_revision", + "condition_permille", + "container_id" + ], + "properties": { + "item_id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "definition_id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "definition_revision": { + "enum": [ + 1, + 2 + ], + "type": "integer" + }, + "condition_permille": { + "type": "integer", + "minimum": 0, + "maximum": 1000 + }, + "container_id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "allOf": [ + { + "if": { + "properties": { + "definition_revision": { + "const": 2 + } + } + }, + "then": { + "properties": { + "definition_id": { + "const": "item.resource-canister.fabricated" + } + } + } + } + ] + } + }, + "cargo": { + "maxProperties": 32, + "propertyNames": { + "minLength": 1, + "maxLength": 128, + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/stock" + }, + "type": "object" + } + } + }, + "walls": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "position", + "quarter_turn" + ], + "properties": { + "quarter_turn": { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + "position": { + "type": "array", + "minItems": 3, + "items": false, + "prefixItems": [ + { + "type": "integer", + "multipleOf": 50, + "minimum": -100000, + "maximum": 100000 + }, + { + "const": 0 + }, + { + "type": "integer", + "multipleOf": 50, + "minimum": -100000, + "maximum": 100000 + } + ], + "maxItems": 3 + } + } + }, + "propertyNames": { + "$ref": "#/$defs/id" + } + }, + "recipe_requirements": { + "type": "object", + "maxProperties": 32, + "propertyNames": { + "$ref": "#/$defs/id" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "station", + "inputs", + "energy_joules" + ], + "properties": { + "station": { + "enum": [ + "drill", + "refinery", + "chemical", + "printer", + "recycler", + "dispenser", + "construction" + ] + }, + "inputs": { + "type": "object", + "minProperties": 1, + "maxProperties": 32, + "propertyNames": { + "enum": [ + "ore.iron", + "ice", + "regolith", + "iron", + "tailings", + "water", + "hydrogen", + "oxygen", + "salts", + "silicate", + "nutrient-solution", + "plate" + ] + }, + "additionalProperties": { + "type": "integer", + "minimum": 1, + "maximum": 100000000 + } + }, + "energy_joules": { + "type": "integer", + "minimum": 1, + "maximum": 100000 + } + } + } + }, + "timed_canister": { + "$ref": "timed-canister-observation-v1.schema.json" + }, + "horizontal_panels": { + "$ref": "horizontal-panel-observation-v1.schema.json" + }, + "paid_doors": { + "$ref": "urn:frontier-systems:schema:paid-door-observation:v6" + } + }, + "$defs": { + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "stock": { + "type": "object", + "maxProperties": 32, + "propertyNames": { + "minLength": 1, + "maxLength": 128 + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 100000000 + } + }, + "id": { + "pattern": "\\S", + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "description": "Own paid chamber controls and finite three-volume pressure observations.", + "allOf": [ + { + "if": { + "properties": { + "catalog_revision": { + "const": 1 + } + } + }, + "then": { + "properties": { + "deposits": { + "type": "object", + "additionalProperties": false, + "required": [ + "deposit.iron", + "deposit.ice", + "deposit.regolith" + ], + "properties": { + "deposit.iron": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.ice": { + "type": "integer", + "minimum": 0, + "maximum": 5000000 + }, + "deposit.regolith": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + } + } + } + } + } + }, + { + "if": { + "properties": { + "catalog_revision": { + "const": 2 + } + } + }, + "then": { + "properties": { + "deposits": { + "type": "object", + "additionalProperties": false, + "required": [ + "deposit.iron", + "deposit.ice", + "deposit.regolith", + "deposit.iron.outcrop" + ], + "properties": { + "deposit.iron": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.ice": { + "type": "integer", + "minimum": 0, + "maximum": 5000000 + }, + "deposit.regolith": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.iron.outcrop": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + } + } + } + } + } + }, + { + "if": { + "properties": { + "catalog_revision": { + "const": 3 + } + } + }, + "then": { + "properties": { + "deposits": { + "type": "object", + "additionalProperties": false, + "required": [ + "deposit.iron", + "deposit.ice", + "deposit.regolith", + "deposit.iron.outcrop", + "deposit.iron.reinforcement" + ], + "properties": { + "deposit.iron": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.ice": { + "type": "integer", + "minimum": 0, + "maximum": 5000000 + }, + "deposit.regolith": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.iron.outcrop": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + }, + "deposit.iron.reinforcement": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + } + } + } + } + } + }, + { + "if": { + "properties": { + "catalog_revision": { + "const": 4 + } + } + }, + "then": { + "properties": { + "deposits": { + "type": "object", + "additionalProperties": false, + "required": [ + "deposit.iron", + "deposit.ice", + "deposit.regolith", + "deposit.iron.outcrop", + "deposit.iron.reinforcement", + "deposit.iron.chamber" + ], + "properties": { + "deposit.iron": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.ice": { + "type": "integer", + "minimum": 0, + "maximum": 5000000 + }, + "deposit.regolith": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.iron.outcrop": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + }, + "deposit.iron.reinforcement": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + }, + "deposit.iron.chamber": { + "type": "integer", + "minimum": 0, + "maximum": 15000000 + } + } + } + } + } + } + ] +} diff --git a/docs/schema/production-system-v15.schema.json b/docs/schema/production-system-v15.schema.json new file mode 100644 index 00000000..79cbb1a1 --- /dev/null +++ b/docs/schema/production-system-v15.schema.json @@ -0,0 +1,1208 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:frontier-systems:schema:production-system:v15", + "title": "Production paid chamber snapshot v15", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "catalog_revision", + "catalog_fingerprint", + "timebase_hz", + "last_tick", + "state_revision", + "deposits", + "store", + "vessel", + "wall_mass_milligrams", + "energy_joules", + "powered", + "job", + "receipts", + "cargo", + "structures", + "loose_parts", + "machinery", + "walls", + "construction", + "construction_bindings", + "construction_commands", + "panels", + "doors", + "room_bypasses", + "room_reinforcements", + "chamber_kits" + ], + "properties": { + "schema_version": { + "const": 15 + }, + "catalog_revision": { + "enum": [ + 1, + 2, + 3, + 4 + ] + }, + "catalog_fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "timebase_hz": { + "enum": [ + 60, + 72, + 90 + ] + }, + "last_tick": { + "$ref": "#/$defs/revision" + }, + "state_revision": { + "$ref": "#/$defs/revision" + }, + "deposits": {}, + "store": { + "$ref": "#/$defs/stock" + }, + "vessel": { + "$ref": "#/$defs/stock" + }, + "wall_mass_milligrams": { + "type": "integer", + "minimum": 0, + "maximum": 100000000, + "multipleOf": 1000000 + }, + "energy_joules": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "powered": { + "type": "object", + "additionalProperties": false, + "required": [ + "drill", + "refinery", + "chemical", + "printer", + "recycler", + "dispenser", + "construction" + ], + "properties": { + "drill": { + "type": "boolean" + }, + "refinery": { + "type": "boolean" + }, + "chemical": { + "type": "boolean" + }, + "printer": { + "type": "boolean" + }, + "recycler": { + "type": "boolean" + }, + "dispenser": { + "type": "boolean" + }, + "construction": { + "type": "boolean" + } + } + }, + "job": { + "oneOf": [ + { + "type": "object", + "maxProperties": 0 + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "recipe_id", + "deposit_id", + "station", + "inputs", + "outputs", + "remaining_ticks" + ], + "properties": { + "recipe_id": { + "type": "string", + "maxLength": 128 + }, + "deposit_id": { + "type": "string", + "maxLength": 128 + }, + "station": { + "enum": [ + "drill", + "refinery", + "chemical", + "printer", + "recycler", + "dispenser", + "construction" + ] + }, + "inputs": { + "$ref": "#/$defs/stock" + }, + "outputs": { + "$ref": "#/$defs/stock" + }, + "remaining_ticks": { + "type": "integer", + "minimum": 1, + "maximum": 36000 + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "recipe_id", + "deposit_id", + "station", + "inputs", + "outputs", + "remaining_ticks", + "extraction" + ], + "properties": { + "recipe_id": { + "type": "string", + "maxLength": 128 + }, + "deposit_id": { + "type": "string", + "maxLength": 128 + }, + "station": { + "enum": [ + "drill", + "refinery", + "chemical", + "printer", + "recycler", + "dispenser", + "construction" + ] + }, + "inputs": { + "$ref": "#/$defs/stock" + }, + "outputs": { + "$ref": "#/$defs/stock" + }, + "remaining_ticks": { + "type": "integer", + "minimum": 1, + "maximum": 60 + }, + "extraction": { + "$ref": "urn:frontier-systems:schema:tool-extraction-work:v1" + } + } + } + ] + }, + "receipts": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "request_id", + "fingerprint", + "state_revision", + "tick_number" + ], + "properties": { + "request_id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "state_revision": { + "$ref": "#/$defs/revision" + }, + "tick_number": { + "$ref": "#/$defs/revision" + } + } + } + }, + "cargo": { + "maxProperties": 32, + "propertyNames": { + "minLength": 1, + "maxLength": 128, + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/stock" + }, + "type": "object" + }, + "structures": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "object", + "required": [ + "blueprint_id", + "fingerprint", + "materials" + ], + "properties": { + "materials": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "integer", + "minimum": 1, + "maximum": 100000000 + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + } + }, + "fingerprint": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + "blueprint_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + } + }, + "additionalProperties": false + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + } + }, + "loose_parts": { + "type": "object", + "additionalProperties": { + "minProperties": 1, + "maxProperties": 32, + "propertyNames": { + "$ref": "#/$defs/id" + }, + "additionalProperties": { + "maximum": 100000000, + "minimum": 1, + "type": "integer" + }, + "type": "object" + }, + "maxProperties": 128, + "propertyNames": { + "$ref": "#/$defs/id" + } + }, + "machinery": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "catalog_fingerprint", + "parts", + "mounts", + "placements", + "links", + "jobs" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "catalog_fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "parts": { + "type": "object", + "maxProperties": 128, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind" + ], + "properties": { + "kind": { + "enum": [ + "frame", + "socket", + "power", + "controller", + "printhead", + "refinery", + "extractor", + "solar", + "router", + "expander", + "cable", + "autolathe" + ] + } + } + } + }, + "mounts": { + "type": "object", + "maxProperties": 128, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "placements": { + "type": "object", + "maxProperties": 20, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "position", + "mode", + "enabled", + "energy", + "generation_remainder" + ], + "properties": { + "position": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "type": "integer", + "minimum": -10000000, + "maximum": 10000000 + } + }, + "mode": { + "enum": [ + "freeform", + "surface" + ] + }, + "enabled": { + "type": "boolean" + }, + "energy": { + "type": "integer", + "minimum": 0, + "maximum": 20000 + }, + "generation_remainder": { + "type": "integer", + "minimum": 0, + "maximum": 89 + } + } + } + }, + "links": { + "type": "object", + "maxProperties": 24, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "b": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + } + } + }, + "jobs": { + "type": "object", + "maxProperties": 20, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "recipe", + "progress", + "energy_spent", + "paused" + ], + "properties": { + "recipe": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "progress": { + "type": "integer", + "minimum": 0, + "maximum": 36000 + }, + "energy_spent": { + "type": "integer", + "minimum": 0, + "maximum": 100000 + }, + "paused": { + "type": "boolean" + } + } + } + } + } + }, + "walls": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "position", + "quarter_turn" + ], + "properties": { + "quarter_turn": { + "type": "integer", + "minimum": 0, + "maximum": 1 + }, + "position": { + "type": "array", + "minItems": 3, + "items": false, + "prefixItems": [ + { + "type": "integer", + "multipleOf": 50, + "minimum": -100000, + "maximum": 100000 + }, + { + "const": 0 + }, + { + "type": "integer", + "multipleOf": 50, + "minimum": -100000, + "maximum": 100000 + } + ], + "maxItems": 3 + } + } + }, + "propertyNames": { + "$ref": "#/$defs/id" + } + }, + "construction": { + "$ref": "construction-reservation-v1.schema.json" + }, + "construction_bindings": { + "type": "object", + "maxProperties": 16, + "propertyNames": { + "$ref": "#/$defs/id" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "actor_id", + "body_id", + "world_id", + "region_id", + "control_epoch", + "fingerprint" + ], + "properties": { + "actor_id": { + "$ref": "#/$defs/id" + }, + "body_id": { + "$ref": "#/$defs/id" + }, + "world_id": { + "$ref": "#/$defs/id" + }, + "region_id": { + "$ref": "#/$defs/id" + }, + "control_epoch": { + "$ref": "#/$defs/revision" + }, + "fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + } + } + } + }, + "construction_commands": { + "type": "object", + "maxProperties": 128, + "propertyNames": { + "$ref": "#/$defs/id" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "request_id", + "fingerprint", + "state_revision", + "tick_number" + ], + "properties": { + "request_id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "fingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "state_revision": { + "$ref": "#/$defs/revision" + }, + "tick_number": { + "$ref": "#/$defs/revision" + } + } + } + }, + "panels": { + "type": "object", + "maxProperties": 64, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "bounds_millimetres", + "height_millimetres", + "blueprint_id", + "blueprint_revision", + "principal_id", + "materials" + ], + "properties": { + "kind": { + "enum": [ + "floor", + "ceiling" + ] + }, + "bounds_millimetres": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": { + "type": "integer", + "minimum": -100500, + "maximum": 100500, + "multipleOf": 50 + } + }, + "height_millimetres": { + "enum": [ + 0, + 2000 + ] + }, + "blueprint_id": { + "const": "surface.horizontal-panel" + }, + "blueprint_revision": { + "const": 1 + }, + "principal_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "materials": { + "type": "object", + "additionalProperties": false, + "required": [ + "plate" + ], + "properties": { + "plate": { + "type": "integer", + "minimum": 2500, + "maximum": 1000000, + "multipleOf": 2500 + } + } + } + } + } + }, + "doors": { + "type": "object", + "maxProperties": 16, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "position", + "quarter_turn", + "principal_id", + "body_id", + "blueprint_id", + "blueprint_revision", + "materials", + "seal" + ], + "properties": { + "position": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "prefixItems": [ + { + "type": "integer", + "minimum": -100000, + "maximum": 100000, + "multipleOf": 50 + }, + { + "const": 0 + }, + { + "type": "integer", + "minimum": -100000, + "maximum": 100000, + "multipleOf": 50 + } + ], + "items": false + }, + "quarter_turn": { + "enum": [ + 0, + 1 + ] + }, + "principal_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "body_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "blueprint_id": { + "const": "door.manual.prototype" + }, + "blueprint_revision": { + "const": 1 + }, + "materials": { + "type": "object", + "additionalProperties": false, + "required": [ + "plate" + ], + "properties": { + "plate": { + "const": 2000000 + } + } + }, + "seal": { + "oneOf": [ + { + "type": "object", + "maxProperties": 0 + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "seal_id", + "blueprint_id", + "blueprint_revision", + "materials" + ], + "properties": { + "seal_id": { + "type": "string", + "pattern": "^door-seal-", + "minLength": 11, + "maxLength": 128 + }, + "blueprint_id": { + "const": "door.manual.seal.prototype" + }, + "blueprint_revision": { + "const": 1 + }, + "materials": { + "type": "object", + "additionalProperties": false, + "required": [ + "plate" + ], + "properties": { + "plate": { + "const": 200000 + } + } + } + } + } + ] + } + } + } + }, + "room_bypasses": { + "type": "object", + "minProperties": 0, + "maxProperties": 16, + "propertyNames": { + "pattern": "^door-", + "maxLength": 128 + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "bypass_id", + "blueprint_id", + "blueprint_revision", + "materials" + ], + "properties": { + "bypass_id": { + "type": "string", + "pattern": "^room-bypass-", + "maxLength": 128 + }, + "blueprint_id": { + "const": "door.manual.bypass.prototype" + }, + "blueprint_revision": { + "const": 1 + }, + "materials": { + "type": "object", + "additionalProperties": false, + "required": [ + "plate" + ], + "properties": { + "plate": { + "const": 200000 + } + } + } + } + } + }, + "room_reinforcements": { + "type": "object", + "minProperties": 0, + "maxProperties": 16, + "propertyNames": { + "pattern": "^door-", + "maxLength": 128 + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "reinforcement_id", + "blueprint_id", + "blueprint_revision", + "seal_id", + "materials" + ], + "properties": { + "blueprint_id": { + "const": "room.reinforcement.prototype" + }, + "blueprint_revision": { + "const": 1 + }, + "materials": { + "type": "object", + "additionalProperties": false, + "required": [ + "plate" + ], + "properties": { + "plate": { + "const": 4000000 + } + } + }, + "reinforcement_id": { + "type": "string", + "pattern": "^room-reinforcement-", + "maxLength": 128 + }, + "seal_id": { + "type": "string", + "pattern": "^door-seal-", + "maxLength": 128 + } + } + } + }, + "chamber_kits": { + "type": "object", + "maxProperties": 1, + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "kit_id", + "inner_door_id", + "outer_door_id", + "inner_seal_id", + "outer_seal_id", + "materials", + "policy_id" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "kit_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^chamber-kit-" + }, + "inner_door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "outer_door_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^door-" + }, + "inner_seal_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "outer_seal_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "\\S" + }, + "materials": { + "type": "object", + "additionalProperties": false, + "required": [ + "plate" + ], + "properties": { + "plate": { + "const": 6000000 + } + } + }, + "policy_id": { + "const": "constructed-chamber.habitat-exterior.prototype.v1" + } + } + } + } + }, + "$defs": { + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740000 + }, + "stock": { + "type": "object", + "maxProperties": 32, + "propertyNames": { + "minLength": 1, + "maxLength": 128 + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 100000000 + } + }, + "id": { + "pattern": "\\S", + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "description": "Distinct paid chamber kit and explicit catalog origin. Runtime validates topology, exact paid bills and all cross-owner links.", + "allOf": [ + { + "if": { + "properties": { + "catalog_revision": { + "const": 1 + } + } + }, + "then": { + "properties": { + "deposits": { + "type": "object", + "additionalProperties": false, + "required": [ + "deposit.iron", + "deposit.ice", + "deposit.regolith" + ], + "properties": { + "deposit.iron": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.ice": { + "type": "integer", + "minimum": 0, + "maximum": 5000000 + }, + "deposit.regolith": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + } + } + } + } + } + }, + { + "if": { + "properties": { + "catalog_revision": { + "const": 2 + } + } + }, + "then": { + "properties": { + "deposits": { + "type": "object", + "additionalProperties": false, + "required": [ + "deposit.iron", + "deposit.ice", + "deposit.regolith", + "deposit.iron.outcrop" + ], + "properties": { + "deposit.iron": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.ice": { + "type": "integer", + "minimum": 0, + "maximum": 5000000 + }, + "deposit.regolith": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.iron.outcrop": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + } + } + } + } + } + }, + { + "if": { + "properties": { + "catalog_revision": { + "const": 3 + } + } + }, + "then": { + "properties": { + "deposits": { + "type": "object", + "additionalProperties": false, + "required": [ + "deposit.iron", + "deposit.ice", + "deposit.regolith", + "deposit.iron.outcrop", + "deposit.iron.reinforcement" + ], + "properties": { + "deposit.iron": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.ice": { + "type": "integer", + "minimum": 0, + "maximum": 5000000 + }, + "deposit.regolith": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.iron.outcrop": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + }, + "deposit.iron.reinforcement": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + } + } + } + } + } + }, + { + "if": { + "properties": { + "catalog_revision": { + "const": 4 + } + } + }, + "then": { + "properties": { + "deposits": { + "type": "object", + "additionalProperties": false, + "required": [ + "deposit.iron", + "deposit.ice", + "deposit.regolith", + "deposit.iron.outcrop", + "deposit.iron.reinforcement", + "deposit.iron.chamber" + ], + "properties": { + "deposit.iron": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.ice": { + "type": "integer", + "minimum": 0, + "maximum": 5000000 + }, + "deposit.regolith": { + "type": "integer", + "minimum": 0, + "maximum": 10000000 + }, + "deposit.iron.outcrop": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + }, + "deposit.iron.reinforcement": { + "type": "integer", + "minimum": 0, + "maximum": 7000000 + }, + "deposit.iron.chamber": { + "type": "integer", + "minimum": 0, + "maximum": 15000000 + } + } + } + } + } + } + ] +} diff --git a/docs/schema/world-snapshot-v1.schema.json b/docs/schema/world-snapshot-v1.schema.json index a9f72f31..805815ac 100644 --- a/docs/schema/world-snapshot-v1.schema.json +++ b/docs/schema/world-snapshot-v1.schema.json @@ -24,7 +24,8 @@ { "$ref": "#/$defs/pressureSystem" }, { "$ref": "urn:frontier-systems:schema:pressure-system:v2" }, { "$ref": "urn:frontier-systems:schema:pressure-system:v3" }, - { "$ref": "urn:frontier-systems:schema:pressure-system:v4" } + { "$ref": "urn:frontier-systems:schema:pressure-system:v4" }, + { "$ref": "urn:frontier-systems:schema:pressure-system:v5" } ] }, "production": { "oneOf": [ { "$ref": "urn:frontier-systems:schema:production-system:v1" }, @@ -40,7 +41,8 @@ { "$ref": "urn:frontier-systems:schema:production-system:v11" }, { "$ref": "urn:frontier-systems:schema:production-system:v12" }, { "$ref": "urn:frontier-systems:schema:production-system:v13" }, - { "$ref": "urn:frontier-systems:schema:production-system:v14" } + { "$ref": "urn:frontier-systems:schema:production-system:v14" }, + { "$ref": "urn:frontier-systems:schema:production-system:v15" } ] }, "health": { "oneOf": [ { "$ref": "urn:frontier-systems:schema:health-system:v1" }, diff --git a/src/adapters/network/door_contact_observation.gd b/src/adapters/network/door_contact_observation.gd index ab5db57e..6c5df726 100644 --- a/src/adapters/network/door_contact_observation.gd +++ b/src/adapters/network/door_contact_observation.gd @@ -5,7 +5,7 @@ class_name DoorContactObservation static func valid_items(value: Variant) -> bool: if typeof(value) != TYPE_DICTIONARY or not ControlContractValidation.has_exact_fields(value, PackedStringArray(["schema_version", "control_epoch", "can_operate", "doors"])): return false if typeof(value.can_operate) != TYPE_BOOL: return false - if not ControlContractValidation.is_integer(value.schema_version, 1, 5) or not ControlContractValidation.is_integer(value.control_epoch, -1, ProductionSimulationSystem.MAX_REVISION) or typeof(value.doors) != TYPE_ARRAY or value.doors.size() > PaidDoorGeometry.MAX_DOORS: return false + if not ControlContractValidation.is_integer(value.schema_version, 1, 6) or not ControlContractValidation.is_integer(value.control_epoch, -1, ProductionSimulationSystem.MAX_REVISION) or typeof(value.doors) != TYPE_ARRAY or value.doors.size() > PaidDoorGeometry.MAX_DOORS: return false var previous: String = "" for d in value.doors: var fields := PackedStringArray(["door_id", "position", "quarter_turn", "opened"]) @@ -13,6 +13,7 @@ static func valid_items(value: Variant) -> bool: if int(value.schema_version) >= 3: fields.append("pressure_room") if int(value.schema_version) >= 4: fields.append_array(["bypass_installed", "bypass_open"]) if int(value.schema_version) >= 5: fields.append_array(["reinforcement_installed", "reinforcement_intact"]) + if int(value.schema_version) >= 6: fields.append_array(["chamber_kit_installed", "chamber"]) if typeof(d) != TYPE_DICTIONARY or not ControlContractValidation.has_exact_fields(d, fields) or not ControlContractValidation.is_id(d.door_id) or not String(d.door_id).begins_with("door-") or typeof(d.opened) != TYPE_BOOL: return false if int(value.schema_version) >= 5 and (typeof(d.reinforcement_installed) != TYPE_BOOL or typeof(d.reinforcement_intact) != TYPE_BOOL or (d.reinforcement_intact and (not d.reinforcement_installed or not d.seal_installed or d.seal_condition != 1.0))): return false if int(value.schema_version) >= 2: @@ -27,6 +28,7 @@ static func valid_items(value: Variant) -> bool: for key in ["volume_m3", "pressure_kpa", "ambient_pressure_kpa"]: if typeof(d.pressure_room[key]) not in [TYPE_INT, TYPE_FLOAT] or not is_finite(float(d.pressure_room[key])) or float(d.pressure_room[key]) < 0.0: return false if d.pressure_room.volume_m3 <= 0.0 or d.pressure_room.volume_m3 > 999.0: return false + if int(value.schema_version) >= 6 and (typeof(d.chamber_kit_installed) != TYPE_BOOL or not valid_chamber(d.chamber, d.door_id)): return false if String(d.door_id) <= previous or not SpatialWallConstruction.valid_pose({"position": d.position, "quarter_turn": d.quarter_turn}): return false previous = d.door_id return true @@ -61,3 +63,14 @@ static func decode(value: Variant, world: StringName, region: StringName) -> Ser static func invalid() -> ServiceResult: return ServiceResult.failure(&"invalid_door_contact", "Expected complete bounded door frame and leaf geometry in the current scope.") + + +static func valid_chamber(value: Variant, door_id: String) -> bool: + if typeof(value) != TYPE_DICTIONARY: return false + if value.is_empty(): return true + if not ControlContractValidation.has_exact_fields(value, PackedStringArray(["inner_door_id", "outer_door_id", "side", "habitat_pressure_kpa", "chamber_pressure_kpa", "exterior_pressure_kpa", "inner_bypass_open", "outer_bypass_open"])): return false + if not ControlContractValidation.is_id(value.inner_door_id) or not ControlContractValidation.is_id(value.outer_door_id) or value.inner_door_id == value.outer_door_id or door_id not in [value.inner_door_id, value.outer_door_id]: return false + if value.side != ("inner" if door_id == value.inner_door_id else "outer") or typeof(value.inner_bypass_open) != TYPE_BOOL or typeof(value.outer_bypass_open) != TYPE_BOOL or (value.inner_bypass_open and value.outer_bypass_open): return false + for field in ["habitat_pressure_kpa", "chamber_pressure_kpa", "exterior_pressure_kpa"]: + if typeof(value[field]) not in [TYPE_INT, TYPE_FLOAT] or not is_finite(float(value[field])) or float(value[field]) < 0.0: return false + return true diff --git a/src/adapters/network/remote_production_projection.gd b/src/adapters/network/remote_production_projection.gd index 2e0b7f2e..090b28f5 100644 --- a/src/adapters/network/remote_production_projection.gd +++ b/src/adapters/network/remote_production_projection.gd @@ -7,7 +7,7 @@ const MATERIALS: Array[String] = ["ore.iron", "ice", "regolith", "iron", "tailin func accept_record(value: Dictionary) -> ServiceResult: - if not ControlContractValidation.is_integer(value.get("schema_version"), 7, 16): return _invalid() + if not ControlContractValidation.is_integer(value.get("schema_version"), 7, 17): return _invalid() var version: int = int(value["schema_version"]) var fields := PackedStringArray(["schema_version", "catalog_revision", "catalog_fingerprint", "timebase_hz", "last_tick", "state_revision", "deposits", "store", "vessel", "wall_mass_milligrams", "energy_joules", "powered", "job", "cargo", "components", "recipes", "dispensable", "can_operate", "vessel_pressure_kpa", "extractable_sources"]) fields.append("machinery") @@ -17,8 +17,8 @@ func accept_record(value: Dictionary) -> ServiceResult: if version >= 9: fields.append("timed_canister") if version >= 10: fields.append("horizontal_panels") if version >= 11: fields.append("paid_doors") - if version >= 11 and (not DoorContactObservation.valid_items(value.get("paid_doors")) or int(value.paid_doors.schema_version) != (5 if version >= 16 else (4 if version >= 15 else (3 if version >= 13 else (2 if version >= 12 else 1))))): return _invalid() - if not ControlContractValidation.has_exact_fields(value, fields) or not ControlContractValidation.is_integer(value.get("schema_version"), 7, 16) or not ControlContractValidation.is_integer(value.get("timebase_hz"), 60, 90) or not [60, 72, 90].has(int(value.get("timebase_hz"))) or typeof(value.get("can_operate")) != TYPE_BOOL: + if version >= 11 and (not DoorContactObservation.valid_items(value.get("paid_doors")) or int(value.paid_doors.schema_version) != (6 if version >= 17 else (5 if version >= 16 else (4 if version >= 15 else (3 if version >= 13 else (2 if version >= 12 else 1)))))): return _invalid() + if not ControlContractValidation.has_exact_fields(value, fields) or not ControlContractValidation.is_integer(value.get("schema_version"), 7, 17) or not ControlContractValidation.is_integer(value.get("timebase_hz"), 60, 90) or not [60, 72, 90].has(int(value.get("timebase_hz"))) or typeof(value.get("can_operate")) != TYPE_BOOL: return _invalid() if not SpatialWallConstruction.valid(value["walls"]): return _invalid() if version >= 10 and not HorizontalPanelObservation.valid(value["horizontal_panels"], value.get("state_revision")): return _invalid() @@ -26,7 +26,7 @@ func accept_record(value: Dictionary) -> ServiceResult: for field in ["last_tick", "state_revision", "catalog_revision", "wall_mass_milligrams", "energy_joules"]: if not ControlContractValidation.is_integer(value[field], 0, 9007199254740000): return _invalid() - if version >= 15 and not ControlContractValidation.is_integer(value["catalog_revision"], 1, 3 if version == 16 else 2): return _invalid() + if version >= 15 and not ControlContractValidation.is_integer(value["catalog_revision"], 1, 4 if version >= 17 else (3 if version == 16 else 2)): return _invalid() var catalog: int = int(value["catalog_revision"]) if version >= 15 else (2 if version == 14 else 1) if not ControlContractValidation.is_integer(value["catalog_revision"], catalog, catalog) or not _fingerprint_valid(value["catalog_fingerprint"]): return _invalid() diff --git a/src/app/first_playable_bootstrap.gd b/src/app/first_playable_bootstrap.gd index 94d49164..c089a1d6 100644 --- a/src/app/first_playable_bootstrap.gd +++ b/src/app/first_playable_bootstrap.gd @@ -717,9 +717,10 @@ func _drop_first_definition(definition_id: String) -> void: ## Shared fresh-world commissioning; callers own attachment and transport startup. -static func create_playable_world(reinforcement_ore: bool = true) -> WorldRuntime: +static func create_playable_world(reinforcement_ore: bool = true, chamber_ore: bool = true) -> WorldRuntime: var world := WorldRuntime.new() world.resource_carriers_enabled = true world.new_world_ore_outcrop_enabled = true world.new_world_reinforcement_ore_enabled = reinforcement_ore + world.new_world_chamber_ore_enabled = reinforcement_ore and chamber_ore return world diff --git a/src/core/access/access_control_system.gd b/src/core/access/access_control_system.gd index d4b2b3be..560131e4 100644 --- a/src/core/access/access_control_system.gd +++ b/src/core/access/access_control_system.gd @@ -17,6 +17,13 @@ const PROTOTYPE_AIRLOCK_ID: StringName = &"habitat_airlock" var devices: Dictionary = {} var current_tick: int = 0 var _door_restore_guard: Callable +var _paid_open_guard: Callable + + +func bind_paid_open_guard(guard: Callable) -> ServiceResult: + if _paid_open_guard.is_valid() or not guard.is_valid(): return ServiceResult.failure(&"door_guard_bound", "Bind one paid opening authority.") + _paid_open_guard = guard + return ServiceResult.success() func bind_door_restore_guard(guard: Callable) -> ServiceResult: @@ -240,6 +247,10 @@ func validate_door_actuation( manual_operation: bool = false, context: AuthenticatedCommandContext = null ) -> ServiceResult: + if opening and _paid_open_guard.is_valid() and String(device_id).begins_with("door-"): + var admitted: ServiceResult = _paid_open_guard.call(String(device_id), context) + if not admitted.is_success: return admitted + pressure_difference_kpa = float(admitted.value) var device := devices.get(device_id) as AccessControlledDeviceState if device == null: return ServiceResult.failure(&"unknown_access_device", "The access-controlled device does not exist.") @@ -278,6 +289,9 @@ func validate_door_actuation( func update_actuator_position(device_id: StringName, target_id: StringName, is_open: bool) -> void: + # Paid geometry is committed through PreparedDoorChange after all owner guards. + # This legacy airlock synchronization hook cannot mutate constructed doors. + if String(device_id).begins_with("door-"): return var device := devices.get(device_id) as AccessControlledDeviceState if device == null: return diff --git a/src/core/access/prepared_door_change.gd b/src/core/access/prepared_door_change.gd index 1770de82..d9678b3f 100644 --- a/src/core/access/prepared_door_change.gd +++ b/src/core/access/prepared_door_change.gd @@ -12,6 +12,31 @@ var _consumed: bool = false var pressure_owner: PressureRoomOwner var pressure_before: Dictionary = {} var pressure_after: Dictionary = {} +var _paired: Dictionary = {} + + +## Stage all additional existing devices before commit. No owner writes here. +func stage_device(id: StringName, after: Dictionary) -> ServiceResult: + if _consumed or id == _id or _paired.has(id): return denied() + var live: AccessControlledDeviceState = _owner.devices.get(id) + var parsed: ServiceResult = AccessControlledDeviceState.from_record(after) + if live == null or not parsed.is_success or parsed.value.device_id != id: return denied() + _paired[id] = {"previous": live, "before": live.to_record().duplicate(true), "after": after.duplicate(true)} + return ServiceResult.success() + + +func project_access(record: Dictionary) -> Dictionary: + var result: Dictionary = record.duplicate(true) + var found: bool = false + for index in range(result.devices.size() - 1, -1, -1): + var id := StringName(result.devices[index].device_id) + if id == _id: + found = true + if _remove: result.devices.remove_at(index) + else: result.devices[index] = _after.duplicate(true) + elif _paired.has(id): result.devices[index] = _paired[id].after.duplicate(true) + if not found and not _remove: result.devices.append(_after.duplicate(true)) + return result func _init(owner: AccessControlSystem, id: StringName, previous: AccessControlledDeviceState, after: Dictionary, remove: bool) -> void: @@ -23,7 +48,7 @@ func _init(owner: AccessControlSystem, id: StringName, previous: AccessControlle _remove = remove -## Every failure precedes the one affected-device write. No callbacks/restore. +## Every failure precedes all affected-device writes. No callbacks/restore. func commit(owner: AccessControlSystem) -> ServiceResult: if _consumed or owner != _owner: return denied() _consumed = true @@ -37,11 +62,20 @@ func commit(owner: AccessControlSystem) -> ServiceResult: if not _remove: parsed = AccessControlledDeviceState.from_record(_after) if not parsed.is_success or parsed.value.device_id != _id: return denied() + var paired_decoded: Dictionary = {} + for id in _paired: + var stage: Dictionary = _paired[id] + var current: AccessControlledDeviceState = owner.devices.get(id) + if current != stage.previous or current == null or current.to_record() != stage.before: return denied() + var decoded: ServiceResult = AccessControlledDeviceState.from_record(stage.after) + if not decoded.is_success or decoded.value.device_id != id: return denied() + paired_decoded[id] = decoded.value + if _remove and live == null: return denied() if _remove: - if live == null: return denied() owner.devices.erase(_id) else: owner.devices[_id] = parsed.value + for id in paired_decoded: owner.devices[id] = paired_decoded[id] # All decoding/CAS/capacity work precedes the first write. No callbacks/yield. if pressure_owner != null and not pressure_after.is_empty(): pressure_owner.install_prepared_room_state(pressure_after) return ServiceResult.success() diff --git a/src/core/pressure/pressure_room_owner.gd b/src/core/pressure/pressure_room_owner.gd index f5c7ea46..b8884607 100644 --- a/src/core/pressure/pressure_room_owner.gd +++ b/src/core/pressure/pressure_room_owner.gd @@ -22,3 +22,7 @@ extends SimulationSystem @abstract func room_observation(door_id: String) -> Dictionary @abstract func room_valve_open(door_id: String) -> bool + +@abstract func validate_room_open(door_id: String, bypass: bool = false) -> ServiceResult + +@abstract func chamber_observation(door_id: String) -> Dictionary diff --git a/src/core/pressure/pressure_system.gd b/src/core/pressure/pressure_system.gd index 3ffdbc32..99efef13 100644 --- a/src/core/pressure/pressure_system.gd +++ b/src/core/pressure/pressure_system.gd @@ -16,6 +16,7 @@ var sensors: Dictionary = {} var total_transferred_moles: float = 0.0 var constructed_rooms: Dictionary = {} var room_valves: Dictionary = {} +var paid_chamber: Dictionary = {} var _room_production: ProductionSimulationSystem var _room_access: AccessControlSystem var _room_world: StringName @@ -155,7 +156,7 @@ func capture_snapshot() -> ServiceResult: sensor_records.append(sensor.to_record()) var result: Dictionary = { - "schema_version": _room_snapshot_version(constructed_rooms, room_valves), + "schema_version": 5 if not paid_chamber.is_empty() else _room_snapshot_version(constructed_rooms, room_valves), "volumes": volume_records, "airlocks": airlock_records, "sensors": sensor_records, @@ -163,6 +164,7 @@ func capture_snapshot() -> ServiceResult: "constructed_rooms": constructed_rooms.duplicate(true), } if int(result.schema_version) >= 3: result["room_valves"] = room_valves.duplicate(true) + if not paid_chamber.is_empty(): result.paid_chamber = paid_chamber.duplicate(true) return ServiceResult.success(result) @@ -192,6 +194,7 @@ func install_prepared_room_state(state: Dictionary) -> void: total_transferred_moles = state["total_transferred_moles"] constructed_rooms = state["constructed_rooms"] room_valves = state["room_valves"] + paid_chamber = state.get("paid_chamber", {}) func _step_transfer(first_id: StringName, second_id: StringName, conductance: float, delta_seconds: float) -> void: @@ -236,18 +239,28 @@ func _sample_sensors(tick_number: int) -> void: func _decode_snapshot_record(record: Dictionary, snapshot_tick_number: int) -> ServiceResult: - if not ControlContractValidation.is_integer(record.get("schema_version"), 1, 4): + if not ControlContractValidation.is_integer(record.get("schema_version"), 1, 5): return ServiceResult.failure(&"unsupported_pressure_system_version", "The pressure-system record version is not supported.") + var paid_record: Variant = record.get("paid_chamber", {}) + if typeof(paid_record) != TYPE_DICTIONARY: return PaidChamber.denied() + if int(record.schema_version) == 5: + if not PaidChamber.valid_record(paid_record): return PaidChamber.denied() + elif not paid_record.is_empty(): return PaidChamber.denied() var rooms: Variant = record.get("constructed_rooms", {}) var fields := PackedStringArray(["schema_version", "volumes", "airlocks", "sensors", "total_transferred_moles"]) if int(record.schema_version) >= 2: fields.append("constructed_rooms") if int(record.schema_version) >= 3: fields.append("room_valves") + if int(record.schema_version) == 5: fields.append("paid_chamber") var valves: Variant = record.get("room_valves", {}) if typeof(rooms) != TYPE_DICTIONARY or rooms.size() > 1: return ConstructedRoomPressure.denied() - if typeof(valves) != TYPE_DICTIONARY or valves.size() > 1: return ConstructedRoomPressure.denied() + if typeof(valves) != TYPE_DICTIONARY or valves.size() > (2 if int(record.schema_version) == 5 else 1): return ConstructedRoomPressure.denied() if int(record.schema_version) == 3 and valves.is_empty(): return ConstructedRoomPressure.denied() if int(record.schema_version) == 4 and (rooms.size() != 1 or not rooms.values().all(func(r: Variant) -> bool: return typeof(r) == TYPE_DICTIONARY and r.get("policy_id") == PaidRoomReinforcement.POLICY)): return ConstructedRoomPressure.denied() + if not paid_record.is_empty() and (not rooms.is_empty() or not ControlContractValidation.has_exact_fields(valves, PackedStringArray([paid_record.inner_door_id, paid_record.outer_door_id]))): return PaidChamber.denied() for id in valves: + if not paid_record.is_empty(): + if typeof(valves[id]) != TYPE_BOOL: return PaidChamber.denied() + continue if typeof(valves[id]) != TYPE_BOOL or not rooms.values().any(func(r: Variant) -> bool: return typeof(r) == TYPE_DICTIONARY and r.get("door_id") == id): return ConstructedRoomPressure.denied() if not ControlContractValidation.has_exact_fields(record, fields) or typeof(rooms) != TYPE_DICTIONARY or rooms.size() > 1: return ConstructedRoomPressure.denied() var volume_values: Variant = record.get("volumes") @@ -346,6 +359,17 @@ func _decode_snapshot_record(record: Dictionary, snapshot_tick_number: int) -> S var ambient: PressureVolumeState = decoded_volumes.get(PROTOTYPE_HABITAT_ID) var sensor: PressureSensorState = decoded_sensors[StringName(room.sensor_id)] if ambient == null or sensor.volume_id != StringName(id) or absf(room_volume.volume_m3 - float(ConstructedRoomPressure.assess(room.geometry).value)) > 1e-9 or room_volume.temperature_kelvin != ambient.temperature_kelvin or absf(ambient.volume_m3 + room_volume.volume_m3 - ConstructedRoomPressure.HABITAT_VOLUME) > 1e-9 or ambient.volume_m3 < 1.0: return ConstructedRoomPressure.denied() + if not paid_record.is_empty(): + var cv: PressureVolumeState = decoded_volumes.get(StringName(paid_record.volume_id)) + var habitat: PressureVolumeState = decoded_volumes.get(PROTOTYPE_HABITAT_ID) + var exterior: PressureVolumeState = decoded_volumes.get(PROTOTYPE_EXTERIOR_ID) + var sensor: PressureSensorState = decoded_sensors.get(StringName(paid_record.sensor_id)) + if cv == null or habitat == null or exterior == null or sensor == null or sensor.volume_id != cv.volume_id or cv.volume_m3 != PaidChamber.VOLUME or absf(habitat.volume_m3 + cv.volume_m3 - 1000.0) > 1e-9 or cv.temperature_kelvin != habitat.temperature_kelvin or cv.temperature_kelvin != exterior.temperature_kelvin: return PaidChamber.denied() + if _room_production != null and (paid_record.world_id != String(_room_world) or paid_record.region_id != String(_room_region)): return PaidChamber.denied() + for id in decoded_volumes: + if String(id).begins_with("paid-chamber-") and (paid_record.is_empty() or String(id) != paid_record.volume_id): return PaidChamber.denied() + for id in decoded_sensors: + if String(id).begins_with("paid-chamber-") and (paid_record.is_empty() or String(id) != paid_record.sensor_id): return PaidChamber.denied() for id in decoded_volumes: if String(id).begins_with("constructed-room-") and not rooms.has(String(id)): return ConstructedRoomPressure.denied() for id in decoded_sensors: @@ -363,6 +387,7 @@ func _decode_snapshot_record(record: Dictionary, snapshot_tick_number: int) -> S "total_transferred_moles": float(transferred_value), "constructed_rooms": rooms.duplicate(true), "room_valves": valves.duplicate(true), + "paid_chamber": paid_record.duplicate(true), }) @@ -379,12 +404,16 @@ func bind_room_owners(production: Node, access: Node, world: StringName, region: func room_area(door_id: String) -> float: + if _chamber_contains(door_id): return ConstructedRoomPressure.AREA for room in constructed_rooms.values(): if room.door_id == door_id: return ConstructedRoomPressure.AREA return 0.0 func room_difference(door_id: String) -> float: + if _chamber_contains(door_id): + var endpoint: StringName = PROTOTYPE_HABITAT_ID if door_id == paid_chamber.inner_door_id else PROTOTYPE_EXTERIOR_ID + return absf((volumes[StringName(paid_chamber.volume_id)] as PressureVolumeState).pressure_kpa() - (volumes[endpoint] as PressureVolumeState).pressure_kpa()) for room in constructed_rooms.values(): if room.door_id == door_id: return absf((volumes[StringName(room.volume_id)] as PressureVolumeState).pressure_kpa() - (volumes[PROTOTYPE_HABITAT_ID] as PressureVolumeState).pressure_kpa()) @@ -394,11 +423,17 @@ func room_difference(door_id: String) -> float: func validate_room_links(production: Dictionary, access: Dictionary, pressure_record: Dictionary = {}, tick: int = 0, inventory: Dictionary = {}) -> ServiceResult: var rooms: Dictionary = constructed_rooms var valves: Dictionary = room_valves + var chamber: Dictionary = paid_chamber if not pressure_record.is_empty(): var decoded: ServiceResult = _decode_snapshot_record(pressure_record, tick) if not decoded.is_success: return decoded rooms = decoded.value.constructed_rooms valves = decoded.value.room_valves + chamber = decoded.value.paid_chamber + if not chamber.is_empty(): + if not rooms.is_empty(): return PaidChamber.denied() + return PaidChamber.links(chamber, valves, production, access, inventory) + if not PaidChamber.valid_kits(production.get("chamber_kits", {}), production): return PaidChamber.denied() var expected: Dictionary = {} for room in rooms.values(): if production.get("room_bypasses", {}).has(room.door_id): expected[room.door_id] = false @@ -408,7 +443,8 @@ func validate_room_links(production: Dictionary, access: Dictionary, pressure_re func record_from_prepared(state: Dictionary) -> Dictionary: var result: Dictionary = {"schema_version": 2, "volumes": [], "airlocks": [], "sensors": [], "total_transferred_moles": state.total_transferred_moles, "constructed_rooms": state.constructed_rooms.duplicate(true)} - result.schema_version = _room_snapshot_version(state.constructed_rooms, state.room_valves) + result.schema_version = 5 if not state.get("paid_chamber", {}).is_empty() else _room_snapshot_version(state.constructed_rooms, state.room_valves) + if int(result.schema_version) == 5: result.paid_chamber = state.paid_chamber.duplicate(true) if int(result.schema_version) >= 3: result["room_valves"] = state.room_valves.duplicate(true) for field in ["volumes", "airlocks", "sensors"]: for id in _sorted_ids(state[field]): result[field].append(state[field][id].to_record()) @@ -416,6 +452,7 @@ func record_from_prepared(state: Dictionary) -> Dictionary: func prepare_room_change(context: AuthenticatedCommandContext, request: String, operation: String, door_id: String, production: Dictionary, tick: int) -> ServiceResult: + if operation in ["door_chamber_commission", "door_chamber_decommission"] or (_chamber_contains(door_id) and operation in ["door_bypass_open", "door_bypass_close"]): return _prepare_chamber(context, request, operation, door_id, production, tick) if context == null or not context.is_valid_for(_room_world, _room_region) or _room_region != &"local-habitat": return ConstructedRoomPressure.denied() var captured: ServiceResult = capture_snapshot() if not captured.is_success: return captured @@ -425,7 +462,7 @@ func prepare_room_change(context: AuthenticatedCommandContext, request: String, if volume.volume_id == String(PROTOTYPE_HABITAT_ID): ambient = volume if ambient.is_empty() or ambient.temperature_kelvin != PROTOTYPE_TEMPERATURE_K: return ConstructedRoomPressure.denied() if operation == "door_room_commission": - if not constructed_rooms.is_empty() or absf(float(ambient.volume_m3) - ConstructedRoomPressure.HABITAT_VOLUME) > 1e-9: return ConstructedRoomPressure.denied() + if not paid_chamber.is_empty() or not constructed_rooms.is_empty() or absf(float(ambient.volume_m3) - ConstructedRoomPressure.HABITAT_VOLUME) > 1e-9: return ConstructedRoomPressure.denied() var body: String = production.doors[door_id].body_id var geometry: Dictionary = ConstructedRoomPressure.geometry(production, String(context.principal_id), body) var assessed: ServiceResult = ConstructedRoomPressure.assess(geometry) @@ -489,6 +526,11 @@ func prepare_room_change(context: AuthenticatedCommandContext, request: String, func room_observation(door_id: String) -> Dictionary: + if _chamber_contains(door_id): + var cv: PressureVolumeState = volumes[StringName(paid_chamber.volume_id)] + var endpoint: StringName = PROTOTYPE_HABITAT_ID if door_id == paid_chamber.inner_door_id else PROTOTYPE_EXTERIOR_ID + var device: AccessControlledDeviceState = _room_access.devices[StringName(door_id)] + return {"volume_m3": cv.volume_m3, "pressure_kpa": cv.pressure_kpa(), "ambient_pressure_kpa": (volumes[endpoint] as PressureVolumeState).pressure_kpa(), "leaking": not PaidChamber.retained(device.actuator(PaidDoorConstruction.actuator_id(door_id)), room_difference(door_id))} for room in constructed_rooms.values(): if room.door_id != door_id: continue var volume: PressureVolumeState = volumes[StringName(room.volume_id)] @@ -503,7 +545,93 @@ func room_valve_open(door_id: String) -> bool: return room_valves.get(door_id, false) +func _chamber_contains(door_id: String) -> bool: + return not paid_chamber.is_empty() and door_id in [paid_chamber.inner_door_id, paid_chamber.outer_door_id] + + +func validate_room_open(door_id: String, bypass: bool = false) -> ServiceResult: + if not _chamber_contains(door_id): return ServiceResult.success() + if _room_access == null or _room_production == null: return PaidChamber.denied() + var captured: ServiceResult = _room_access.capture_snapshot() + if not captured.is_success or not validate_room_links(_room_production._state, captured.value).is_success: return PaidChamber.denied() + var opposite: String = paid_chamber.outer_door_id if door_id == paid_chamber.inner_door_id else paid_chamber.inner_door_id + if not PaidChamber.closed_intact(PaidChamber.actuator(captured.value, opposite)) or room_valve_open(opposite): return PaidChamber.denied() + if bypass: + if not PaidChamber.closed_intact(PaidChamber.actuator(captured.value, door_id)): return PaidChamber.denied() + elif room_valve_open(door_id): return PaidChamber.denied() + return ServiceResult.success() + + +func _step_paid_chamber(delta: float) -> void: + if _room_access == null or _room_production == null: return + var access: ServiceResult = _room_access.capture_snapshot() + if not access.is_success or not validate_room_links(_room_production._state, access.value).is_success: return + # Fixed inner then outer order; max(leak,valve), never two summed paths. + for id in [paid_chamber.inner_door_id, paid_chamber.outer_door_id]: + var target: ActuatorState = PaidChamber.actuator(access.value, id) + if room_valve_open(id) or not PaidChamber.retained(target, room_difference(id)): + _step_transfer(StringName(paid_chamber.volume_id), PROTOTYPE_HABITAT_ID if id == paid_chamber.inner_door_id else PROTOTYPE_EXTERIOR_ID, ConstructedRoomPressure.CONDUCTANCE, delta) + + +func _prepare_chamber(context: AuthenticatedCommandContext, request: String, operation: String, door_id: String, production: Dictionary, tick: int) -> ServiceResult: + if context == null or not context.is_valid_for(_room_world, _room_region) or _room_region != &"local-habitat": return PaidChamber.denied() + var captured: ServiceResult = capture_snapshot() + if not captured.is_success: return captured + var record: Dictionary = captured.value.duplicate(true) + var ambient: Dictionary = {} + for volume in record.volumes: + if volume.volume_id == String(PROTOTYPE_HABITAT_ID): ambient = volume + if ambient.is_empty() or ambient.temperature_kelvin != PROTOTYPE_TEMPERATURE_K: return PaidChamber.denied() + if operation == "door_chamber_commission": + if not paid_chamber.is_empty() or not constructed_rooms.is_empty() or ambient.volume_m3 != 1000.0: return PaidChamber.denied() + var kit: Dictionary = PaidChamber.kit_for(production, door_id) + if kit.is_empty() or kit.inner_door_id != door_id: return PaidChamber.denied() + var door: Dictionary = production.doors[door_id] + var geometry: Dictionary = ConstructedRoomPressure.geometry(production, String(context.principal_id), door.body_id) + if not PaidChamber.assess(geometry).is_success: return PaidChamber.denied() + var id: String = "paid-chamber-%s" % JSON.stringify([String(context.principal_id), request]).sha256_text().substr(0, 32) + if volumes.has(StringName(id)) or sensors.has(StringName("%s-sensor" % id)): return PaidChamber.denied() + var moles: float = float(ambient.gas_moles) * PaidChamber.VOLUME / float(ambient.volume_m3) + ambient.gas_moles -= moles + ambient.volume_m3 -= PaidChamber.VOLUME + record.volumes.append(PressureVolumeState.new(StringName(id), PaidChamber.VOLUME, ambient.temperature_kelvin, moles).to_record()) + record.sensors.append(PressureSensorState.new(StringName("%s-sensor" % id), StringName(id)).to_record()) + record.paid_chamber = {"schema_version": 1, "policy_id": PaidChamber.POLICY, "volume_id": id, "sensor_id": "%s-sensor" % id, "principal_id": String(context.principal_id), "body_id": door.body_id, "world_id": String(context.world_id), "region_id": String(context.region_id), "inner_door_id": door_id, "outer_door_id": kit.outer_door_id, "inner_endpoint": PaidChamber.INNER, "outer_endpoint": PaidChamber.OUTER, "geometry": geometry} + record.room_valves = {door_id: false, kit.outer_door_id: false} + record.schema_version = 5 + else: + if not _chamber_contains(door_id) or paid_chamber.principal_id != String(context.principal_id): return PaidChamber.denied() + if operation == "door_chamber_decommission": + if door_id != paid_chamber.inner_door_id: return PaidChamber.denied() + for id in [paid_chamber.inner_door_id, paid_chamber.outer_door_id]: + if room_valve_open(id) or room_difference(id) * 1710.0 > AccessControlSystem.PROTOTYPE_MANUAL_FORCE_NEWTONS: return PaidChamber.denied() + for volume in record.volumes: + if volume.volume_id == paid_chamber.volume_id: + ambient.gas_moles += volume.gas_moles + ambient.volume_m3 += volume.volume_m3 + record.volumes = record.volumes.filter(func(v: Dictionary) -> bool: return v.volume_id != paid_chamber.volume_id) + record.sensors = record.sensors.filter(func(v: Dictionary) -> bool: return v.sensor_id != paid_chamber.sensor_id) + record.erase("paid_chamber") + record.erase("room_valves") + record.schema_version = 2 + elif operation in ["door_bypass_open", "door_bypass_close"]: + if operation == "door_bypass_open" and not validate_room_open(door_id, true).is_success: return PaidChamber.denied() + record.room_valves[door_id] = operation == "door_bypass_open" + else: return PaidChamber.denied() + var decoded: ServiceResult = _decode_snapshot_record(record, tick) + if not decoded.is_success: return decoded + var before_moles: float = 0.0 + var after_moles: float = 0.0 + for volume in captured.value.volumes: before_moles += float(volume.gas_moles) + for volume in record.volumes: after_moles += float(volume.gas_moles) + if absf(before_moles - after_moles) > maxf(1e-9, before_moles * 1e-12): return PaidChamber.denied() + return ServiceResult.success({"before": captured.value, "after": decoded.value}) + + func _step_constructed_room(delta: float) -> void: + if not paid_chamber.is_empty(): + _step_paid_chamber(delta) + return if constructed_rooms.is_empty(): return # Broken links stop flow, never erase gas or guess another ambient endpoint. if _room_production == null or _room_access == null: return @@ -592,3 +720,8 @@ static func _room_snapshot_version(rooms: Dictionary, valves: Dictionary) -> int for room in rooms.values(): if room.policy_id == PaidRoomReinforcement.POLICY: return 4 return 2 if valves.is_empty() else 3 + + +func chamber_observation(door_id: String) -> Dictionary: + if not _chamber_contains(door_id): return {} + return {"inner_door_id": paid_chamber.inner_door_id, "outer_door_id": paid_chamber.outer_door_id, "side": "inner" if door_id == paid_chamber.inner_door_id else "outer", "habitat_pressure_kpa": (volumes[PROTOTYPE_HABITAT_ID] as PressureVolumeState).pressure_kpa(), "chamber_pressure_kpa": (volumes[StringName(paid_chamber.volume_id)] as PressureVolumeState).pressure_kpa(), "exterior_pressure_kpa": (volumes[PROTOTYPE_EXTERIOR_ID] as PressureVolumeState).pressure_kpa(), "inner_bypass_open": room_valve_open(paid_chamber.inner_door_id), "outer_bypass_open": room_valve_open(paid_chamber.outer_door_id)} diff --git a/src/core/production/paid_chamber.gd b/src/core/production/paid_chamber.gd new file mode 100644 index 00000000..fe21abfa --- /dev/null +++ b/src/core/production/paid_chamber.gd @@ -0,0 +1,167 @@ +extends RefCounted +class_name PaidChamber + +## Narrow first-party logical pressure connection, not spatial atmosphere. +const MASS: int = 6000000 +const POLICY: String = "constructed-chamber.habitat-exterior.prototype.v1" +const OPERATIONS: Array[String] = ["door_chamber_kit_install", "door_chamber_kit_recover", "door_chamber_commission", "door_chamber_decommission"] +const INNER: String = "habitat_main" +const OUTER: String = "exterior_reservoir" +const VOLUME: float = 3.6 +const RETENTION_KPA: float = 110.0 + + +static func assess(geometry: Dictionary) -> ServiceResult: + if not ControlContractValidation.has_exact_fields(geometry, PackedStringArray(["walls", "panels", "doors"])): return denied() + if typeof(geometry.walls) != TYPE_DICTIONARY or typeof(geometry.panels) != TYPE_DICTIONARY or typeof(geometry.doors) != TYPE_DICTIONARY: return denied() + if not SpatialWallConstruction.valid(geometry.walls) or not HorizontalPanelConstruction.valid(geometry.panels) or not PaidDoorGeometry.valid(geometry.doors, geometry.panels) or geometry.doors.size() != 2: return denied() + var walls: ServiceResult = ConstructedWallBoundaries.describe(geometry.walls, 0) + if not walls.is_success: return denied() + var surfaces: Array[Dictionary] = [] + for id in geometry.panels: surfaces.append(HorizontalPanelConstruction.descriptor(id, geometry.panels[id])) + var openings: Array[Dictionary] = [] + var inner: String = "" + var outer: String = "" + for id in geometry.doors: + var door: Dictionary = geometry.doors[id] + if door.quarter_turn != 1: return denied() + if door.position == [5050, 0, -4000]: inner = id + elif door.position == [6950, 0, -4000]: outer = id + else: return denied() + openings.append({"door_id": id, "position": door.position.duplicate(), "quarter_turn": door.quarter_turn}) + var result: ServiceResult = ConstructedDoorOpeningTopology.assess({"schema_version": 1, "production_revision": 0, "walls": walls.value, "surfaces": surfaces, "openings": openings}) + if not result.is_success or result.value.status != "enclosure_with_unsealed_openings" or result.value.interior_bounds_millimetres != [5100, 6900, -4500, -3500] or inner.is_empty() or outer.is_empty(): return denied() + return ServiceResult.success({"inner_door_id": inner, "outer_door_id": outer}) + + +static func candidate(production: Dictionary, principal: String, body: String) -> ServiceResult: + return assess(ConstructedRoomPressure.geometry(production, principal, body)) + + +static func valid_kits(value: Variant, production: Dictionary) -> bool: + if typeof(value) != TYPE_DICTIONARY or value.size() > 1: return false + for id in value: + var kit: Variant = value[id] + if typeof(kit) != TYPE_DICTIONARY or not ControlContractValidation.has_exact_fields(kit, PackedStringArray(["schema_version", "kit_id", "inner_door_id", "outer_door_id", "inner_seal_id", "outer_seal_id", "materials", "policy_id"])): return false + if not ControlContractValidation.is_integer(kit.schema_version, 1, 1) or kit.policy_id != POLICY or kit.inner_door_id != id or kit.outer_door_id == id or typeof(kit.materials) != TYPE_DICTIONARY or kit.materials != {"plate": MASS}: return false + for field in ["kit_id", "inner_door_id", "outer_door_id", "inner_seal_id", "outer_seal_id"]: + if not ControlContractValidation.is_id(kit[field]): return false + if not ControlContractValidation.is_integer(kit.materials.plate, MASS, MASS): return false + if not ControlContractValidation.is_id(kit.kit_id) or not String(kit.kit_id).begins_with("chamber-kit-"): return false + var doors: Dictionary = production.get("doors", {}) + if not doors.has(id) or not doors.has(kit.outer_door_id): return false + var inner: Dictionary = doors[id] + var outer: Dictionary = doors[kit.outer_door_id] + if inner.principal_id != outer.principal_id or inner.body_id != outer.body_id: return false + for boundary in [id, kit.outer_door_id]: + if not production.get("room_bypasses", {}).has(boundary) or production.get("room_reinforcements", {}).has(boundary): return false + if inner.seal.is_empty() or outer.seal.is_empty() or inner.seal.seal_id != kit.inner_seal_id or outer.seal.seal_id != kit.outer_seal_id: return false + var pair: ServiceResult = candidate(production, inner.principal_id, inner.body_id) + if not pair.is_success or pair.value.inner_door_id != id or pair.value.outer_door_id != kit.outer_door_id: return false + return true + + +static func kit_for(production: Dictionary, door: String) -> Dictionary: + for kit in production.get("chamber_kits", {}).values(): + if door in [kit.inner_door_id, kit.outer_door_id]: return kit + return {} + + +static func valid_record(value: Variant) -> bool: + if typeof(value) != TYPE_DICTIONARY or not ControlContractValidation.has_exact_fields(value, PackedStringArray(["schema_version", "policy_id", "volume_id", "sensor_id", "principal_id", "body_id", "world_id", "region_id", "inner_door_id", "outer_door_id", "inner_endpoint", "outer_endpoint", "geometry"])): return false + if not ControlContractValidation.is_integer(value.schema_version, 1, 1) or value.policy_id != POLICY or value.region_id != "local-habitat" or value.inner_endpoint != INNER or value.outer_endpoint != OUTER: return false + for field in ["volume_id", "sensor_id", "principal_id", "body_id", "world_id", "inner_door_id", "outer_door_id"]: + if not ControlContractValidation.is_id(value[field]): return false + if not String(value.volume_id).begins_with("paid-chamber-") or value.sensor_id != "%s-sensor" % value.volume_id or typeof(value.geometry) != TYPE_DICTIONARY: return false + var pair: ServiceResult = assess(value.geometry) + if not pair.is_success or pair.value.inner_door_id != value.inner_door_id or pair.value.outer_door_id != value.outer_door_id: return false + for door in value.geometry.doors.values(): + if door.principal_id != value.principal_id or door.body_id != value.body_id or door.seal.is_empty(): return false + return true + + +static func actuator(access: Dictionary, id: String) -> ActuatorState: + for record in access.get("devices", []): + if record.get("device_id") != id: continue + var parsed: ServiceResult = AccessControlledDeviceState.from_record(record) + return parsed.value.actuator(PaidDoorConstruction.actuator_id(id)) if parsed.is_success else null + return null + + +static func closed_intact(target: ActuatorState) -> bool: + return target != null and target.position == 0.0 and target.integrity == 1.0 and target.seal_condition == 1.0 and target.mode == &"manual" and target.latch_state == &"released" + + +static func retained(target: ActuatorState, difference: float) -> bool: + return closed_intact(target) and difference <= RETENTION_KPA + ConstructedRoomPressure.TOLERANCE_KPA + + +static func links(chamber: Dictionary, valves: Dictionary, production: Dictionary, access: Dictionary, inventory: Dictionary) -> ServiceResult: + if not valid_kits(production.get("chamber_kits", {}), production): return denied() + if chamber.is_empty(): return PaidDoorConstruction.links(production, access, inventory) + if not valid_record(chamber) or ConstructedRoomPressure.geometry(production, chamber.principal_id, chamber.body_id) != chamber.geometry: return denied() + var kit: Dictionary = kit_for(production, chamber.inner_door_id) + if kit.is_empty() or kit.outer_door_id != chamber.outer_door_id: return denied() + var areas: Dictionary = {} + var opened: int = 0 + var flowing: int = 0 + for id in [chamber.inner_door_id, chamber.outer_door_id]: + if not production.get("room_bypasses", {}).has(id) or not valves.has(id) or typeof(valves[id]) != TYPE_BOOL or production.get("room_reinforcements", {}).has(id): return denied() + var target: ActuatorState = actuator(access, id) + if target == null: return denied() + opened += 1 if target.position != 0.0 else 0 + flowing += 1 if valves[id] else 0 + areas[id] = ConstructedRoomPressure.AREA + if opened > 1 or flowing > 1 or (opened > 0 and flowing > 0): return denied() + return PaidDoorConstruction.links(production, access, inventory, areas) + + +static func denied() -> ServiceResult: + return ServiceResult.failure(&"paid_chamber_denied", "A complete owned paid chamber pair, isolated boundaries and current authority are required.") + + +static func plan(state: Dictionary, inventory: InventorySimulationSystem, access: AccessControlSystem, context: AuthenticatedCommandContext, request: String, operation: String, payload: Dictionary, pressure: PressureRoomOwner) -> ServiceResult: + if not ControlContractValidation.has_exact_fields(payload, PackedStringArray(["expected_revision", "control_epoch", "door_id"])) or pressure == null: return denied() + var authorized: ServiceResult = inventory.authorize(context, true) + var tool: ServiceResult = inventory.active_tool(context, "tool.crowbar") + if not authorized.is_success or not tool.is_success: return denied() + var geometry: Dictionary = ConstructedRoomPressure.geometry(state, String(context.principal_id), authorized.value) + var pair: ServiceResult = assess(geometry) + if not pair.is_success or payload.door_id != pair.value.inner_door_id: return denied() + var ids: Array = [pair.value.inner_door_id, pair.value.outer_door_id] + var devices: Dictionary = {} + for id in ids: + var door: Dictionary = state.doors[id] + var current: AccessControlledDeviceState = access.devices.get(StringName(id)) + if current == null or not PaidDoorConstruction.valid_device(id, door, current.to_record(), pressure.room_area(id)): return denied() + var device: AccessControlledDeviceState = AccessControlledDeviceState.from_record(current.to_record()).value + if not closed_intact(device.actuator(PaidDoorConstruction.actuator_id(id))) or not device.bypass_jobs.is_empty() or not device.tamper_events.is_empty() or device.actuator(PaidDoorConstruction.actuator_id(id)).override_actor_id != &"": return denied() + if not PaidDoorConstruction.reachable(inventory, context, door.position).is_success: return denied() + if not PaidDoorConstruction.unoccupied(door, inventory.capture_snapshot().value, state): return denied() + if state.get("room_reinforcements", {}).has(id): return denied() + devices[id] = device + var kits: Dictionary = state.get("chamber_kits", {}).duplicate(true) + var inner: String = ids[0] + var outer: String = ids[1] + if operation == "door_chamber_kit_install": + if not kits.is_empty() or pressure.room_area(inner) != 0.0 or pressure.room_area(outer) != 0.0 or int(state.store.plate) < MASS: return denied() + for id in ids: + if not state.get("room_bypasses", {}).has(id): return denied() + kits[inner] = {"schema_version": 1, "kit_id": "chamber-kit-%s" % JSON.stringify([String(context.principal_id), request]).sha256_text().substr(0, 32), "inner_door_id": inner, "outer_door_id": outer, "inner_seal_id": state.doors[inner].seal.seal_id, "outer_seal_id": state.doors[outer].seal.seal_id, "materials": {"plate": MASS}, "policy_id": POLICY} + state.store.plate -= MASS + elif operation == "door_chamber_kit_recover": + if not kits.has(inner) or pressure.room_area(inner) != 0.0 or ProductionSimulationSystem._mass(state.store) + ProductionSimulationSystem._reserved_mass(state) + MASS > ProductionSimulationSystem.MAX_MASS: return denied() + state.store.plate += MASS + kits.erase(inner) + else: + if not kits.has(inner): return denied() + for id in ids: + if not state.get("room_bypasses", {}).has(id) or pressure.room_valve_open(id): return denied() + if operation == "door_chamber_decommission" and pressure.room_difference(id) * 1000.0 * ConstructedRoomPressure.AREA > AccessControlSystem.PROTOTYPE_MANUAL_FORCE_NEWTONS: return denied() + devices[id].actuator(PaidDoorConstruction.actuator_id(id)).effective_pressure_area_m2 = ConstructedRoomPressure.AREA if operation == "door_chamber_commission" else 0.0 + state.chamber_kits = kits + state.schema_version = 15 + if not state.has("room_reinforcements"): state.room_reinforcements = {} + var prepared := PreparedDoorChange.new(access, StringName(inner), access.devices[StringName(inner)], devices[inner].to_record(), false) + var staged: ServiceResult = prepared.stage_device(StringName(outer), devices[outer].to_record()) + return ServiceResult.success(prepared) if staged.is_success else staged diff --git a/src/core/production/paid_door_construction.gd b/src/core/production/paid_door_construction.gd index d5f9a972..94f2c151 100644 --- a/src/core/production/paid_door_construction.gd +++ b/src/core/production/paid_door_construction.gd @@ -94,6 +94,18 @@ static func unoccupied(pose: Dictionary, inventory: Dictionary, state: Dictionar return true +## Prototype manual handle is900mm above its paid floor anchor. Own canonical +## body_center normalizes legacy y0 and real capsule-center y900 consistently. +static func reachable(inventory: InventorySimulationSystem, context: AuthenticatedCommandContext, position: Array) -> ServiceResult: + var authorized: ServiceResult = inventory.production_capability(context, true, []) + if not authorized.is_success: return authorized + var pose: ServiceResult = inventory.own_position(context) + if not pose.is_success: return pose + var handle: Vector3 = SpatialWallConstruction.point(position) + Vector3(0, 900, 0) + if SpatialWallConstruction.body_center(pose.value).distance_to(handle) > InventorySimulationSystem.WORLD_REACH_MILLIMETRES: return failure("The server-owned manual handle is beyond1500mm reach.") + return authorized + + static func plan(state: Dictionary, inventory: InventorySimulationSystem, access: AccessControlSystem, context: AuthenticatedCommandContext, request: String, operation: String, payload: Dictionary, pressure: PressureRoomOwner = null) -> ServiceResult: var create: bool = operation == "door_build" var fields: Array = ["expected_revision", "control_epoch", "position", "quarter_turn"] if create else ["expected_revision", "control_epoch", "door_id"] @@ -120,6 +132,7 @@ static func plan(state: Dictionary, inventory: InventorySimulationSystem, access if not current.is_success or not valid_device(id, door, current.value, pressure.room_area(id) if pressure != null else 0.0): return failure("The commissioned door hardware is invalid.") device = AccessControlledDeviceState.from_record(current.value).value var actuator: ActuatorState = device.actuator(actuator_id(id)) + if not PaidChamber.kit_for(state, id).is_empty() and operation in ["door_remove", "door_seal_install", "door_seal_remove", "door_room_commission", "door_reinforcement_install", "door_bypass_recover"]: return failure("Recover the distinct paid chamber kit before changing its linked boundary.") if pressure != null and pressure.room_area(id) > 0.0 and operation in ["door_remove", "door_seal_install", "door_seal_remove"]: return failure("Decommission the pressure compartment before changing its paid boundary.") if operation == "door_remove": if state.get("room_reinforcements", {}).has(id): return failure("Recover the paid reinforcement before removing its door.") @@ -136,7 +149,7 @@ static func plan(state: Dictionary, inventory: InventorySimulationSystem, access var reinforcement: ServiceResult = PaidRoomReinforcement.plan(state, id, actuator, device, inventory, context, request, operation, pressure) if not reinforcement.is_success: return reinforcement elif operation in PaidRoomBypass.OPERATIONS: - var bypass: ServiceResult = PaidRoomBypass.plan(state, id, actuator, device, inventory, context, request, operation, pressure) + var bypass: ServiceResult = PaidRoomBypass.plan(state, id, actuator, device, inventory, context, request, operation, pressure, access) if not bypass.is_success: return bypass elif operation in ["door_room_commission", "door_room_decommission"]: var tool: ServiceResult = inventory.active_tool(context, "tool.crowbar") @@ -156,7 +169,7 @@ static func plan(state: Dictionary, inventory: InventorySimulationSystem, access var can_move: ServiceResult = access.validate_door_actuation(StringName(id), actuator_id(id), operation == "door_open", pressure.room_difference(id) if pressure != null else 0.0, true, verified) if not can_move.is_success: return can_move actuator.position = 1.0 if operation == "door_open" else 0.0 - var reach: ServiceResult = inventory.production_capability(context, true, door["position"]) + var reach: ServiceResult = reachable(inventory, context, door["position"]) if not reach.is_success: return reach if not clear_installations(doors, state) or not unoccupied(door, inventory.capture_snapshot().value, state): return failure("The installation swing is occupied or has an unsupported canonical shape.") if operation == "door_remove": doors.erase(id) diff --git a/src/core/production/paid_room_bypass.gd b/src/core/production/paid_room_bypass.gd index 478fe0a2..a83b2b54 100644 --- a/src/core/production/paid_room_bypass.gd +++ b/src/core/production/paid_room_bypass.gd @@ -28,13 +28,23 @@ static func mass(state: Dictionary) -> int: return state.get("room_bypasses", {}).size() * MASS -static func plan(state: Dictionary, door_id: String, actuator: ActuatorState, device: AccessControlledDeviceState, inventory: InventorySimulationSystem, context: AuthenticatedCommandContext, request: String, operation: String, pressure: PressureRoomOwner) -> ServiceResult: +static func plan(state: Dictionary, door_id: String, actuator: ActuatorState, device: AccessControlledDeviceState, inventory: InventorySimulationSystem, context: AuthenticatedCommandContext, request: String, operation: String, pressure: PressureRoomOwner, access: AccessControlSystem) -> ServiceResult: var tool: ServiceResult = inventory.active_tool(context, "tool.crowbar") if not tool.is_success: return tool if pressure == null or actuator.mode != &"manual" or actuator.integrity <= 0.0 or actuator.latch_state != &"released" or not device.bypass_jobs.is_empty() or not device.tamper_events.is_empty() or actuator.override_actor_id != &"": return PaidDoorConstruction.failure("Usable manual hardware and cleared work are required.") var parts: Dictionary = state.get("room_bypasses", {}).duplicate(true) + var candidate_pair: bool = false + if pressure.room_area(door_id) == 0.0 and operation == "door_bypass_install": + var authorized: ServiceResult = inventory.authorize(context, true) + if authorized.is_success: + var pair: ServiceResult = PaidChamber.candidate(state, String(context.principal_id), authorized.value) + candidate_pair = pair.is_success and door_id in [pair.value.inner_door_id, pair.value.outer_door_id] and PaidChamber.closed_intact(actuator) + if candidate_pair: + for id in [pair.value.inner_door_id, pair.value.outer_door_id]: + var other: AccessControlledDeviceState = access.devices.get(StringName(id)) + if other == null or pressure.room_area(id) != 0.0 or not PaidChamber.closed_intact(other.actuator(PaidDoorConstruction.actuator_id(id))): candidate_pair = false if operation == "door_bypass_install": - if parts.has(door_id) or pressure.room_area(door_id) == 0.0 or actuator.position != 0.0 or int(state.store.plate) < MASS: return PaidDoorConstruction.failure("A closed commissioned door and 0.2 kg unreserved plate are required.") + if parts.has(door_id) or (pressure.room_area(door_id) == 0.0 and not candidate_pair) or actuator.position != 0.0 or int(state.store.plate) < MASS: return PaidDoorConstruction.failure("A closed commissioned door and 0.2 kg unreserved plate are required.") parts[door_id] = {"bypass_id": "room-bypass-%s" % JSON.stringify([String(context.principal_id), request]).sha256_text().substr(0, 32), "blueprint_id": BLUEPRINT, "blueprint_revision": 1, "materials": {"plate": MASS}} state.store.plate -= MASS elif operation == "door_bypass_recover": @@ -45,7 +55,7 @@ static func plan(state: Dictionary, door_id: String, actuator: ActuatorState, de parts.erase(door_id) else: if not parts.has(door_id) or pressure.room_area(door_id) == 0.0: return PaidDoorConstruction.failure("Install a paid bypass on the commissioned door first.") - if int(state.schema_version) == 14: + if int(state.schema_version) >= 14: state.room_bypasses = parts elif parts.is_empty(): state.erase("room_bypasses") diff --git a/src/core/production/paid_room_reinforcement.gd b/src/core/production/paid_room_reinforcement.gd index 4692cfa2..8781fb93 100644 --- a/src/core/production/paid_room_reinforcement.gd +++ b/src/core/production/paid_room_reinforcement.gd @@ -50,5 +50,5 @@ static func plan(state: Dictionary, door_id: String, actuator: ActuatorState, de parts.erase(door_id) state.room_reinforcements = parts if not state.has("room_bypasses"): state.room_bypasses = {} - state.schema_version = 14 + state.schema_version = maxi(int(state.schema_version), 14) return ServiceResult.success() diff --git a/src/core/production/production_simulation_system.gd b/src/core/production/production_simulation_system.gd index bfb7acfa..f42798ce 100644 --- a/src/core/production/production_simulation_system.gd +++ b/src/core/production/production_simulation_system.gd @@ -18,9 +18,11 @@ var _room_pressure: PressureRoomOwner var _checkpoint_door_links: Dictionary = {} const STATIONS: Array[String] = ["drill", "refinery", "chemical", "printer", "recycler", "dispenser", "construction"] ## Stable fixture sites, relative to the inventory display origin in millimetres. -const SOURCE_POSITIONS: Dictionary = {"deposit.iron": [-3000, 0, -1000], "deposit.ice": [-6000, 0, -1000], "deposit.regolith": [-9000, 0, -1000], "deposit.iron.outcrop": [-3000, 0, -1000], "deposit.iron.reinforcement": [-3000, 0, -1000]} +const SOURCE_POSITIONS: Dictionary = {"deposit.iron": [-3000, 0, -1000], "deposit.ice": [-6000, 0, -1000], "deposit.regolith": [-9000, 0, -1000], "deposit.iron.outcrop": [-3000, 0, -1000], "deposit.iron.reinforcement": [-3000, 0, -1000], "deposit.iron.chamber": [-3000, 0, -1000]} var _catalog: Dictionary = {} var _catalog_fingerprint: String = "" +var _chamber_catalog: Dictionary = {} +var _chamber_fingerprint: String = "" var _reinforced_catalog: Dictionary = {} var _reinforced_fingerprint: String = "" var _legacy_catalog: Dictionary = {} @@ -39,7 +41,7 @@ func _init() -> void: tick_priority = 300 -func configure(inventory: InventorySimulationSystem, tools: ToolSimulationSystem = null, new_world_outcrop: bool = false, new_world_reinforcement: bool = false) -> ServiceResult: +func configure(inventory: InventorySimulationSystem, tools: ToolSimulationSystem = null, new_world_outcrop: bool = false, new_world_reinforcement: bool = false, new_world_chamber: bool = false) -> ServiceResult: if not _state.is_empty() or inventory == null or MachineryWorkshop.KINDS.is_empty(): return _failure("Configure one inventory capability provider.") var raw: Variant = JSON.parse_string(FileAccess.get_file_as_string("res://data/production/catalog.v2.json")) @@ -56,6 +58,9 @@ func configure(inventory: InventorySimulationSystem, tools: ToolSimulationSystem _reinforced_catalog = JSON.parse_string(FileAccess.get_file_as_string("res://data/production/catalog.v3.json")) if not _validate_catalog(_reinforced_catalog).is_success: return _failure("Reinforced production catalog is invalid.") _reinforced_fingerprint = JSON.stringify(_reinforced_catalog, "", true, true).sha256_text() + _chamber_catalog = JSON.parse_string(FileAccess.get_file_as_string("res://data/production/catalog.v4.json")) + if not _validate_catalog(_chamber_catalog).is_success: return _failure("Chamber catalog is invalid.") + _chamber_fingerprint = JSON.stringify(_chamber_catalog, "", true, true).sha256_text() _catalog = raw.duplicate(true) _inventory = inventory _tools = tools @@ -64,24 +69,25 @@ func configure(inventory: InventorySimulationSystem, tools: ToolSimulationSystem return blueprints var timed: ServiceResult = _timed_blueprints.configure(JSON.parse_string(FileAccess.get_file_as_string("res://data/production/timed-canister-blueprint.v1.json")), inventory, _catalog["materials"]) if not timed.is_success: return timed - _state = initial_record(0, new_world_outcrop, new_world_reinforcement) + _state = initial_record(0, new_world_outcrop, new_world_reinforcement, new_world_chamber) var motion: ServiceResult = inventory.bind_motion_validator(func(from: Array, to: Array) -> bool: return SpatialWallConstruction.motion_clear(_state["walls"], from, to) and door_motion_clear(from, to)) if not motion.is_success: return motion return inventory.bind_payload_mass_provider(payload_mass) -func initial_record(tick: int, new_world_outcrop: bool = false, new_world_reinforcement: bool = false) -> Dictionary: +func initial_record(tick: int, new_world_outcrop: bool = false, new_world_reinforcement: bool = false, new_world_chamber: bool = false) -> Dictionary: var powered: Dictionary = {} for station in STATIONS: powered[station] = true - var revision: int = 3 if new_world_reinforcement else (2 if new_world_outcrop else 1) - var result: Dictionary = {"schema_version": 14 if new_world_reinforcement else (12 if new_world_outcrop else 11), "doors": {}, "panels": {}, "construction": ConstructionReservationPlan.initial(), "construction_bindings": {}, "construction_commands": {}, "walls": {}, "machinery": MachineryWorkshop.initial(), "loose_parts": {}, "structures": {}, "catalog_revision": revision, "catalog_fingerprint": _reinforced_fingerprint if revision == 3 else (_catalog_fingerprint if revision == 2 else _legacy_fingerprint), "timebase_hz": Engine.physics_ticks_per_second, - "last_tick": tick, "state_revision": 0, "deposits": _initial_deposits(new_world_outcrop, new_world_reinforcement), "store": _empty_stock(), + var revision: int = 4 if new_world_chamber else (3 if new_world_reinforcement else (2 if new_world_outcrop else 1)) + var result: Dictionary = {"schema_version": 15 if new_world_chamber else (14 if new_world_reinforcement else (12 if new_world_outcrop else 11)), "doors": {}, "panels": {}, "construction": ConstructionReservationPlan.initial(), "construction_bindings": {}, "construction_commands": {}, "walls": {}, "machinery": MachineryWorkshop.initial(), "loose_parts": {}, "structures": {}, "catalog_revision": revision, "catalog_fingerprint": _chamber_fingerprint if revision == 4 else (_reinforced_fingerprint if revision == 3 else (_catalog_fingerprint if revision == 2 else _legacy_fingerprint)), "timebase_hz": Engine.physics_ticks_per_second, + "last_tick": tick, "state_revision": 0, "deposits": _initial_deposits(new_world_outcrop, new_world_reinforcement, new_world_chamber), "store": _empty_stock(), "vessel": _empty_stock(), "wall_mass_milligrams": 0, "energy_joules": 100000, "powered": powered, "job": {}, "receipts": [], "cargo": {}} - if revision == 3: + if revision >= 3: result.room_bypasses = {} result.room_reinforcements = {} + if revision == 4: result.chamber_kits = {} return result @@ -183,9 +189,9 @@ func validate_inventory_links(record: Dictionary, inventory_record: Dictionary) return ServiceResult.success() -func _initial_deposits(new_world_outcrop: bool = false, new_world_reinforcement: bool = false) -> Dictionary: +func _initial_deposits(new_world_outcrop: bool = false, new_world_reinforcement: bool = false, new_world_chamber: bool = false) -> Dictionary: var result: Dictionary = {} - var catalog: Dictionary = _reinforced_catalog if new_world_reinforcement else (_catalog if new_world_outcrop else _legacy_catalog) + var catalog: Dictionary = _chamber_catalog if new_world_chamber else (_reinforced_catalog if new_world_reinforcement else (_catalog if new_world_outcrop else _legacy_catalog)) for id in catalog["deposits"]: result[id] = int(catalog["deposits"][id]["mass_milligrams"]) return result @@ -349,7 +355,8 @@ func observation(context: AuthenticatedCommandContext) -> ServiceResult: # Observation version is independent of the v9 persistence record. observed.erase("room_bypasses") observed.erase("room_reinforcements") - observed["schema_version"] = 16 if int(_state.schema_version) == 14 else (15 if _state.has("room_bypasses") else (14 if int(_state["catalog_revision"]) == 2 else 13)) + observed.erase("chamber_kits") + observed["schema_version"] = 17 if int(_state.schema_version) == 15 else (16 if int(_state.schema_version) == 14 else (15 if _state.has("room_bypasses") else (14 if int(_state["catalog_revision"]) == 2 else 13))) observed["paid_doors"] = door_observation(context) observed["horizontal_panels"] = HorizontalPanelObservation.project(_state, context, construction_closure(context), _inventory.production_capability(context, true, []).is_success) observed["recipes"] = _catalog["recipes"].keys() @@ -608,7 +615,7 @@ func _prepare_tool_extraction(candidate: Dictionary, context: AuthenticatedComma candidate["job"]["extraction"]["status"] = "running" candidate["job"]["extraction"]["pause_reason"] = "" return ServiceResult.success() - if _tools == null or not _fields(payload, ["expected_revision", "deposit_id", "carrier_id", "cycles"]) or not candidate["deposits"].has(payload.get("deposit_id")) or not ControlContractValidation.is_id(payload.get("carrier_id")) or not ControlContractValidation.is_integer(payload.get("cycles"), 1, ToolExtractionWork.MAX_CYCLES) or not candidate["job"].is_empty() or int(candidate["state_revision"]) > MAX_REVISION - 100: return _failure("Reserve 1–10 extraction cycles with exact source/carrier identities while production is idle.") + if _tools == null or not _fields(payload, ["expected_revision", "deposit_id", "carrier_id", "cycles"]) or not candidate["deposits"].has(payload.get("deposit_id")) or not ControlContractValidation.is_id(payload.get("carrier_id")) or not ControlContractValidation.is_integer(payload.get("cycles"), 1, ToolExtractionWork.MAX_CYCLES) or not candidate["job"].is_empty() or int(candidate["state_revision"]) > MAX_REVISION - 100: return _failure("Reserve 1–10 extraction cycles with exact source/carrier identities while production is idle.") var active: ServiceResult = _inventory.active_tool(context, "tool.extractor") if not active.is_success: return active var binding: Dictionary = {"request_id": request_id, "principal_id": String(context.principal_id), "tool_id": active.value, "carrier_id": payload["carrier_id"], "deposit_id": payload["deposit_id"], "remaining_cycles": int(payload["cycles"]), "completed_cycles": 0, "status": "running", "pause_reason": ""} @@ -908,8 +915,8 @@ func machine_recipe(id: String) -> Dictionary: return recipe if id.begins_with("extract."): var deposit: String = "deposit.%s" % id.trim_prefix("extract.") - if _reinforced_catalog["deposits"].has(deposit): - var material: String = _reinforced_catalog["deposits"][deposit]["material"] + if _chamber_catalog["deposits"].has(deposit): + var material: String = _chamber_catalog["deposits"][deposit]["material"] return {"inputs": {}, "outputs": {material: 1000000}, "duration_ticks": 60, "energy_joules": 1000, "capability": "extractor", "deposit": deposit} return {} @@ -1041,29 +1048,32 @@ func restore_snapshot_record(record: Dictionary, tick: int) -> ServiceResult: func _decode(record: Dictionary, tick: int) -> ServiceResult: var fields: Array = ["schema_version", "catalog_revision", "catalog_fingerprint", "timebase_hz", "last_tick", "state_revision", "deposits", "store", "vessel", "wall_mass_milligrams", "energy_joules", "powered", "job", "receipts"] - if ControlContractValidation.is_integer(record.get("schema_version"), 2, 14): + if ControlContractValidation.is_integer(record.get("schema_version"), 2, 15): fields.append("cargo") - if ControlContractValidation.is_integer(record.get("schema_version"), 3, 14): + if ControlContractValidation.is_integer(record.get("schema_version"), 3, 15): fields.append("structures") - if ControlContractValidation.is_integer(record.get("schema_version"), 4, 14): fields.append("loose_parts") - if ControlContractValidation.is_integer(record.get("schema_version"), 6, 14): fields.append("machinery") - if ControlContractValidation.is_integer(record.get("schema_version"), 7, 14): fields.append("walls") - if ControlContractValidation.is_integer(record.get("schema_version"), 8, 14): fields.append_array(["construction", "construction_bindings", "construction_commands"]) - if ControlContractValidation.is_integer(record.get("schema_version"), 9, 14): fields.append("panels") - if ControlContractValidation.is_integer(record.get("schema_version"), 10, 14): fields.append("doors") - if ControlContractValidation.is_integer(record.get("schema_version"), 13, 14): fields.append("room_bypasses") - if ControlContractValidation.is_integer(record.get("schema_version"), 14, 14): fields.append("room_reinforcements") + if ControlContractValidation.is_integer(record.get("schema_version"), 4, 15): fields.append("loose_parts") + if ControlContractValidation.is_integer(record.get("schema_version"), 6, 15): fields.append("machinery") + if ControlContractValidation.is_integer(record.get("schema_version"), 7, 15): fields.append("walls") + if ControlContractValidation.is_integer(record.get("schema_version"), 8, 15): fields.append_array(["construction", "construction_bindings", "construction_commands"]) + if ControlContractValidation.is_integer(record.get("schema_version"), 9, 15): fields.append("panels") + if ControlContractValidation.is_integer(record.get("schema_version"), 10, 15): fields.append("doors") + if ControlContractValidation.is_integer(record.get("schema_version"), 13, 15): fields.append("room_bypasses") + if ControlContractValidation.is_integer(record.get("schema_version"), 14, 15): fields.append("room_reinforcements") + if ControlContractValidation.is_integer(record.get("schema_version"), 15, 15): fields.append("chamber_kits") if _catalog.is_empty() or not _fields(record, fields): return _failure("An exact configured production snapshot is required.") - if not ControlContractValidation.is_integer(record.schema_version, 1, 14) or not ControlContractValidation.is_integer(record.catalog_revision, 1, 3): return _failure("Unsupported production/catalog version.") + if not ControlContractValidation.is_integer(record.schema_version, 1, 15) or not ControlContractValidation.is_integer(record.catalog_revision, 1, 4): return _failure("Unsupported production/catalog version.") var revision: int = int(record.catalog_revision) - var reinforced: bool = int(record.get("schema_version", 0)) == 14 + var chamber_origin: bool = int(record.schema_version) == 15 + var reinforced: bool = int(record.schema_version) >= 14 + if revision == 4 and not chamber_origin: return _failure("Catalog4 requires explicit save15 origin.") var expanded: bool = revision == 2 if revision == 3 and not reinforced: return _failure("New content requires explicit v14 origin.") - var saved_catalog: Dictionary = _reinforced_catalog if revision == 3 else (_catalog if expanded else _legacy_catalog) - if record["catalog_fingerprint"] != (_reinforced_fingerprint if revision == 3 else (_catalog_fingerprint if expanded else _legacy_fingerprint)): + var saved_catalog: Dictionary = _chamber_catalog if revision == 4 else (_reinforced_catalog if revision == 3 else (_catalog if expanded else _legacy_catalog)) + if record["catalog_fingerprint"] != (_chamber_fingerprint if revision == 4 else (_reinforced_fingerprint if revision == 3 else (_catalog_fingerprint if expanded else _legacy_fingerprint))): return _failure("The saved catalog differs; an explicit production migration is required.") - if not ControlContractValidation.is_integer(record["schema_version"], 1, 14) or not ControlContractValidation.is_integer(record["catalog_revision"], 1, 3) or (not reinforced and ((int(record.schema_version) == 12 and revision != 2) or (int(record.schema_version) < 12 and revision != 1))): + if not ControlContractValidation.is_integer(record["schema_version"], 1, 15) or not ControlContractValidation.is_integer(record["catalog_revision"], 1, 4) or (not reinforced and ((int(record.schema_version) == 12 and revision != 2) or (int(record.schema_version) < 12 and revision != 1))): return _failure("Unsupported production/catalog version.") if not ControlContractValidation.is_integer(record["last_tick"], tick, tick) or not ControlContractValidation.is_integer(record["timebase_hz"], Engine.physics_ticks_per_second, Engine.physics_ticks_per_second) or not [60, 72, 90].has(Engine.physics_ticks_per_second): return _failure("Production tick and supported timebase must match the world.") @@ -1078,7 +1088,7 @@ func _decode(record: Dictionary, tick: int) -> ServiceResult: normalized["loose_parts"] = record.get("loose_parts", {}).duplicate(true) for field in ["schema_version", "catalog_revision", "timebase_hz", "last_tick", "state_revision", "wall_mass_milligrams", "energy_joules"]: normalized[field] = int(record[field]) - normalized["schema_version"] = 14 if reinforced else (13 if int(record["schema_version"]) == 13 else (12 if expanded else 11)) + normalized["schema_version"] = 15 if chamber_origin else (14 if reinforced else (13 if int(record["schema_version"]) == 13 else (12 if expanded else 11))) for field in ["construction", "construction_bindings", "construction_commands"]: var fallback: Dictionary = ConstructionReservationPlan.initial() if field == "construction" else {} if typeof(record.get(field, fallback)) != TYPE_DICTIONARY: return _failure("Construction fragments must be objects.") @@ -1112,6 +1122,7 @@ func _decode(record: Dictionary, tick: int) -> ServiceResult: for wall in normalized["walls"].values(): wall["position"] = InventorySpatialMath.normalized(wall["position"]) wall["quarter_turn"] = int(wall["quarter_turn"]) + if chamber_origin and not PaidChamber.valid_kits(record.chamber_kits, normalized): return _failure("Invalid paid chamber kit or geometry.") var machinery: Variant = record.get("machinery", MachineryWorkshop.initial()) if typeof(machinery) != TYPE_DICTIONARY: return _failure("Machinery must be a record object.") normalized["machinery"] = machinery.duplicate(true) @@ -1125,7 +1136,7 @@ func _decode(record: Dictionary, tick: int) -> ServiceResult: if typeof(structures) != TYPE_DICTIONARY or structures.size() > 32: return _failure("Structural equipment records must be bounded.") normalized["structures"] = {} - var structural_mass: int = PaidRoomReinforcement.mass(normalized) + PaidRoomBypass.mass(normalized) + normalized["doors"].size() * PaidDoorGeometry.MASS + PaidDoorSeal.mass(normalized["doors"]) + HorizontalPanelConstruction.mass(normalized["panels"]) + MachineryWorkshop.mass(normalized["machinery"]) + normalized["walls"].size() * SpatialWallConstruction.MASS + var structural_mass: int = normalized.get("chamber_kits", {}).size() * PaidChamber.MASS + PaidRoomReinforcement.mass(normalized) + PaidRoomBypass.mass(normalized) + normalized["doors"].size() * PaidDoorGeometry.MASS + PaidDoorSeal.mass(normalized["doors"]) + HorizontalPanelConstruction.mass(normalized["panels"]) + MachineryWorkshop.mass(normalized["machinery"]) + normalized["walls"].size() * SpatialWallConstruction.MASS if typeof(record.get("loose_parts", {})) != TYPE_DICTIONARY or normalized["loose_parts"].size() > 128: return _failure("Loose parts must be a bounded material map.") for id in normalized["loose_parts"]: if not ControlContractValidation.is_id(id) or not _stock_valid(normalized["loose_parts"][id]): return _failure("Invalid loose part materials.") @@ -1180,7 +1191,7 @@ func _decode(record: Dictionary, tick: int) -> ServiceResult: return _failure("A bounded reserved batch is required.") var expected: Dictionary if typeof(job["deposit_id"]) == TYPE_STRING and saved_catalog["deposits"].has(job["deposit_id"]) and job["recipe_id"] == "": - var material: String = _reinforced_catalog["deposits"][job["deposit_id"]]["material"] + var material: String = saved_catalog["deposits"][job["deposit_id"]]["material"] var mass: int = int(job["extraction"]["remaining_cycles"]) * ToolExtractionWork.CYCLE_MASS if job.has("extraction") else 1000000 expected = {"station": "drill", "inputs": {material: mass}, "outputs": {material: mass}, "duration_ticks": 60} elif job["deposit_id"] == "" and typeof(job["recipe_id"]) == TYPE_STRING and _catalog["recipes"].has(job["recipe_id"]): @@ -1196,7 +1207,7 @@ func _decode(record: Dictionary, tick: int) -> ServiceResult: if _mass(normalized["store"]) + _reserved_mass(normalized) > MAX_MASS or _mass(normalized["vessel"]) > VESSEL_CAPACITY or _vessel_pressure(normalized["vessel"]) > MAX_VESSEL_PRESSURE_KPA: return _failure("Reserved output or vessel exceeds capacity.") var total: int = _mass(normalized["deposits"]) + _mass(normalized["store"]) + _mass(normalized["vessel"]) + int(normalized["wall_mass_milligrams"]) + _reserved_mass(normalized) + carried_mass + structural_mass - if total != _mass(_initial_deposits(expanded, revision == 3)): + if total != _mass(_initial_deposits(expanded, revision == 3, revision == 4)): return _failure("Closed fixture material mass must be conserved, including reserved batches.") if typeof(record["receipts"]) != TYPE_ARRAY or record["receipts"].size() > MAX_RECEIPTS: return _failure("Receipt history exceeds its bound.") @@ -1214,7 +1225,7 @@ func _decode(record: Dictionary, tick: int) -> ServiceResult: receipt["tick_number"] = previous_tick ids[receipt["request_id"]] = true if int(normalized["state_revision"]) == 0: - var initial: Dictionary = initial_record(tick, expanded, revision == 3) + var initial: Dictionary = initial_record(tick, expanded, revision == 3, revision == 4) if normalized != initial: return _failure("Revision-zero production must retain its initial finite fixture state.") elif normalized["receipts"].is_empty(): @@ -1253,10 +1264,10 @@ func _apply_door(context: AuthenticatedCommandContext, request: String, operatio before = captured.value var current_links: ServiceResult = _room_pressure.validate_room_links(_state, _door_access.capture_snapshot().value) if not current_links.is_success: return current_links - var prepared: ServiceResult = PaidDoorConstruction.plan(candidate, _inventory, _door_access, context, request, operation, payload, _room_pressure) + var prepared: ServiceResult = PaidChamber.plan(candidate, _inventory, _door_access, context, request, operation, payload, _room_pressure) if operation in PaidChamber.OPERATIONS else PaidDoorConstruction.plan(candidate, _inventory, _door_access, context, request, operation, payload, _room_pressure) if not prepared.is_success: return prepared var pressure_record: Dictionary = before - if operation in ["door_room_commission", "door_room_decommission", "door_bypass_install", "door_bypass_open", "door_bypass_close", "door_reinforcement_install"]: + if operation in ["door_chamber_commission", "door_chamber_decommission", "door_room_commission", "door_room_decommission", "door_bypass_open", "door_bypass_close", "door_reinforcement_install"] or (operation == "door_bypass_install" and _room_pressure.room_area(payload.door_id) > 0.0): var pressure_plan: ServiceResult = _room_pressure.prepare_room_change(context, request, operation, payload.door_id, candidate, tick) if not pressure_plan.is_success: return pressure_plan prepared.value.pressure_after = pressure_plan.value.after @@ -1265,15 +1276,7 @@ func _apply_door(context: AuthenticatedCommandContext, request: String, operatio if _room_pressure != null: prepared.value.pressure_owner = _room_pressure prepared.value.pressure_before = before - var after_access: Dictionary = _door_access.capture_snapshot().value.duplicate(true) - var found: bool = false - for index in range(after_access.devices.size()): - if after_access.devices[index].device_id == String(prepared.value._id): - found = true - if prepared.value._remove: after_access.devices.remove_at(index) - else: after_access.devices[index] = prepared.value._after.duplicate(true) - break - if not found and not prepared.value._remove: after_access.devices.append(prepared.value._after.duplicate(true)) + var after_access: Dictionary = prepared.value.project_access(_door_access.capture_snapshot().value) if not _room_pressure.validate_room_links(candidate, after_access, pressure_record, tick).is_success: return _failure("Prepared pressure, construction and hardware links differ.") if not TimedCanisterConstruction.covered(candidate): return _failure("Unfinished construction retains its reserved stock.") if not PaidDoorGeometry.valid(candidate["doors"], candidate["panels"]): return _failure("Door support validation failed.") @@ -1315,7 +1318,7 @@ func bind_room_pressure(pressure: PressureRoomOwner, world: StringName, region: var bound: ServiceResult = pressure.bind_room_owners(self, _door_access, world, region) if not bound.is_success: return bound _room_pressure = pressure - return ServiceResult.success() + return _door_access.bind_paid_open_guard(_admit_paid_open) func _validate_constructed_links(production: Dictionary, access: Dictionary) -> ServiceResult: @@ -1349,12 +1352,16 @@ func door_observation(context: AuthenticatedCommandContext) -> Dictionary: for item in items: item["bypass_installed"] = _state.room_bypasses.has(item.door_id) item["bypass_open"] = _room_pressure.room_valve_open(item.door_id) if _room_pressure != null else false - if int(_state.schema_version) == 14: + if int(_state.schema_version) >= 14: for item in items: item["reinforcement_installed"] = _state.room_reinforcements.has(item.door_id) var actuator: ActuatorState = _door_access.devices[StringName(item.door_id)].actuator(PaidDoorConstruction.actuator_id(item.door_id)) item["reinforcement_intact"] = item.reinforcement_installed and PaidRoomReinforcement.intact(actuator) - return {"schema_version": 5 if int(_state.schema_version) == 14 else (4 if _state.has("room_bypasses") else 3), "control_epoch": epoch, "can_operate": authorize_door(context, {"control_epoch": epoch}).is_success, "doors": items} + if int(_state.schema_version) >= 15: + for item in items: + item["chamber_kit_installed"] = not PaidChamber.kit_for(_state, item.door_id).is_empty() + item["chamber"] = _room_pressure.chamber_observation(item.door_id) if _room_pressure != null else {} + return {"schema_version": 6 if int(_state.schema_version) >= 15 else (5 if int(_state.schema_version) == 14 else (4 if _state.has("room_bypasses") else 3)), "control_epoch": epoch, "can_operate": authorize_door(context, {"control_epoch": epoch}).is_success, "doors": items} func door_solids() -> ServiceResult: @@ -1502,4 +1509,16 @@ static func source_ids(revision: int) -> Array[String]: var ids: Array[String] = ["deposit.iron", "deposit.ice", "deposit.regolith"] if revision >= 2: ids.append("deposit.iron.outcrop") if revision >= 3: ids.append("deposit.iron.reinforcement") + if revision >= 4: ids.append("deposit.iron.chamber") return ids + + +func _admit_paid_open(door_id: String, context: AuthenticatedCommandContext) -> ServiceResult: + if context == null or not _state.doors.has(door_id): return _failure("Unknown paid opening authority.") + var door: Dictionary = _state.doors[door_id] + var auth: ServiceResult = _inventory.authorize(context, true) + if not auth.is_success or door.principal_id != String(context.principal_id) or door.body_id != auth.value: return _failure("Only the constructing current body may open this door.") + if not PaidDoorConstruction.reachable(_inventory, context, door.position).is_success or not _inventory.active_tool(context, "tool.crowbar").is_success: return _failure("Opening requires current reach and the actual held crowbar.") + var safe: ServiceResult = _room_pressure.validate_room_open(door_id) + if not safe.is_success: return safe + return ServiceResult.success(_room_pressure.room_difference(door_id)) diff --git a/src/core/runtime/world_runtime.gd b/src/core/runtime/world_runtime.gd index 29777d08..7fb66262 100644 --- a/src/core/runtime/world_runtime.gd +++ b/src/core/runtime/world_runtime.gd @@ -16,6 +16,7 @@ const DEFAULT_REGION_ID: StringName = &"local-habitat" ## Only fresh playable commissioning; snapshot recovery never grants this source. @export var new_world_ore_outcrop_enabled: bool = false @export var new_world_reinforcement_ore_enabled: bool = false +@export var new_world_chamber_ore_enabled: bool = false var _simulation_runner: SimulationRunner var _local_request_sequence: int = 0 var _local_request_nonce: String = Crypto.new().generate_random_bytes(8).hex_encode() @@ -61,7 +62,7 @@ func _ready() -> void: _simulation_runner.add_child(tool_system) var production_system := ProductionSimulationSystem.new() production_system.name = "ProductionSimulationSystem" - _production_setup_result = production_system.configure(inventory_system, tool_system, new_world_ore_outcrop_enabled, new_world_reinforcement_ore_enabled) + _production_setup_result = production_system.configure(inventory_system, tool_system, new_world_ore_outcrop_enabled, new_world_reinforcement_ore_enabled, new_world_chamber_ore_enabled) if _production_setup_result.is_success: _production_setup_result = production_system.bind_door_access(access_control_system) if _production_setup_result.is_success: _production_setup_result = production_system.bind_room_pressure(pressure_system, world_id, region_id) _simulation_runner.add_child(production_system) diff --git a/src/presentation/habitat/procedural_habitat_preview.gd b/src/presentation/habitat/procedural_habitat_preview.gd index 55577fd1..ea34b2b1 100644 --- a/src/presentation/habitat/procedural_habitat_preview.gd +++ b/src/presentation/habitat/procedural_habitat_preview.gd @@ -10,6 +10,8 @@ var _door_shapes: Dictionary = {} var _doors: Dictionary = {} var _targets: Dictionary = {"inner": 1.2, "outer": 1.2} var _pressure_label: Label3D +var _paid_worksite_legacy_wall: MeshInstance3D +var _paid_worksite_legacy_collider: CollisionShape3D func _ready() -> void: @@ -35,7 +37,11 @@ func _ready() -> void: _box("BackWall", Vector3(0.2, 2.5, 10.0), Vector3(-21.0, 1.25, 0.0), wall_color) for side in [-1.0, 1.0]: _box("HabitatWall", Vector3(20.0, 2.5, 0.2), Vector3(-11.0, 1.25, side * 5.0), wall_color) - _box("DoorFrameWall", Vector3(0.2, 2.5, 4.0), Vector3(-1.0, 1.25, side * 3.0), wall_color) + var frame: MeshInstance3D = _box("DoorFrameWall", Vector3(0.2, 2.5, 4.0), Vector3(-1.0, 1.25, side * 3.0), wall_color, false) + var frame_collider: CollisionShape3D = _collider(frame, "Barrier", Vector3(0.2, 2.5, 4.0), Vector3.ZERO) + if side < 0.0: + _paid_worksite_legacy_wall = frame + _paid_worksite_legacy_collider = frame_collider _box("ChamberWall", Vector3(2.0, 2.5, 0.12), Vector3(0.0, 1.25, side), Color("#96a8b1")) _doors["inner"] = _box("InnerDoor", Vector3(0.12, 2.4, 1.8), Vector3(-1.0, 1.2, 0.0), Color("#52a3ba"), false) _doors["outer"] = _box("OuterDoor", Vector3(0.12, 2.4, 1.8), Vector3(1.0, 1.2, 0.0), Color("#d69356"), false) @@ -69,6 +75,10 @@ func _ready() -> void: func focus_production() -> void: + # Only this obsolete presentation wall intersects the paid worksite at + # world(-0.95,-2). Logical prototype pressure and its doors are unchanged. + if _paid_worksite_legacy_wall != null: _paid_worksite_legacy_wall.visible = false + if _paid_worksite_legacy_collider != null: _paid_worksite_legacy_collider.disabled = true if _pressure_label != null: _pressure_label.visible = false diff --git a/src/presentation/production/paid_door_controls.gd b/src/presentation/production/paid_door_controls.gd index 5c06a422..9bf7f552 100644 --- a/src/presentation/production/paid_door_controls.gd +++ b/src/presentation/production/paid_door_controls.gd @@ -40,9 +40,9 @@ func configure(world: WorldRuntime, local_principal: StringName = &"local-invent _status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _status.text = "Requires a paid floor under the full swing. Stand outside the swing within 1.5 m, with a usable crowbar. Seal: 0.2 kg plate. Commission one complete habitat enclosure for finite pressure service; open before decommissioning." _details.add_child(_status) - for operation in ["door_build", "door_open", "door_close", "door_remove", "door_seal_install", "door_seal_remove", "door_room_commission", "door_room_decommission"] + PaidRoomBypass.OPERATIONS + PaidRoomReinforcement.OPERATIONS: + for operation in ["door_build", "door_open", "door_close", "door_remove", "door_seal_install", "door_seal_remove", "door_room_commission", "door_room_decommission"] + PaidRoomBypass.OPERATIONS + PaidRoomReinforcement.OPERATIONS + PaidChamber.OPERATIONS: var button := Button.new() - button.text = {"door_reinforcement_install": "Fit 110 kPa prototype kit: 4 kg plate", "door_reinforcement_recover": "Recover kit: decommission first", "door_bypass_install": "Fit manual bypass: 0.2 kg plate", "door_bypass_open": "Open bypass to surroundings", "door_bypass_close": "Close bypass", "door_bypass_recover": "Recover bypass: decommission first", "door_room_commission": "Commission habitat compartment", "door_room_decommission": "Decommission open compartment", "door_seal_install": "Fit/rework seal: 0.2 kg plate", "door_seal_remove": "Remove seal: recover 0.2 kg plate", "door_build": "Build manual door", "door_open": "Pry selected door open", "door_close": "Pry selected door closed", "door_remove": "Disassemble selected closed door"}[operation] + button.text = {"door_chamber_kit_install": "Fit chamber kit on inner door: 6 kg plate", "door_chamber_kit_recover": "Recover chamber kit: decommission first", "door_chamber_commission": "Commission paid inner/outer chamber", "door_chamber_decommission": "Dismantle only when both sides pressure-safe; may require habitat depressurization", "door_reinforcement_install": "Fit 110 kPa prototype kit: 4 kg plate", "door_reinforcement_recover": "Recover kit: decommission first", "door_bypass_install": "Fit manual bypass: 0.2 kg plate", "door_bypass_open": "Open bypass to surroundings", "door_bypass_close": "Close bypass", "door_bypass_recover": "Recover bypass: decommission first", "door_room_commission": "Commission habitat compartment", "door_room_decommission": "Decommission open compartment", "door_seal_install": "Fit/rework seal: 0.2 kg plate", "door_seal_remove": "Remove seal: recover 0.2 kg plate", "door_build": "Build manual door", "door_open": "Pry selected door open", "door_close": "Pry selected door closed", "door_remove": "Disassemble selected closed door"}[operation] button.disabled = true button.pressed.connect(_send.bind(operation)) _details.add_child(button) @@ -66,10 +66,10 @@ func _process(delta: float) -> void: var context := AuthenticatedCommandContext.for_local_development(_local_principal, _world.world_id, _world.region_id) var observed: ServiceResult = _world.production_observation(context) _record.clear() - if observed.is_success and ControlContractValidation.is_integer(observed.value.get("schema_version"), 12, 16) and WorkstationResourceView.valid(observed.value): _record = observed.value + if observed.is_success and ControlContractValidation.is_integer(observed.value.get("schema_version"), 12, 17) and WorkstationResourceView.valid(observed.value): _record = observed.value _doors.clear() for door in _record.get("paid_doors", {}).get("doors", []): - _doors.add_item("%s: %s; %s" % [String(door.door_id).right(8), "open" if door.opened else "closed", seal_text(door)]) + _doors.add_item("%s: %s; %s" % [String(door.door_id).right(8), "open" if door.opened else "closed", seal_text(door, _record.paid_doors.doors)]) _doors.set_item_metadata(_doors.item_count - 1, door.door_id) if _selected == door.door_id: _doors.select(_doors.item_count - 1) if _doors.item_count > 0 and _selected.is_empty(): _selected = _doors.get_item_metadata(0) @@ -92,7 +92,14 @@ func _send(operation: String) -> void: if not queued.is_success: _pending = ""; _status.text = queued.message -static func seal_text(door: Dictionary) -> String: +static func seal_text(door: Dictionary, doors: Array = []) -> String: + if not door.get("chamber", {}).is_empty(): + var c: Dictionary = door.chamber + var opposite: String = c.outer_door_id if c.side == "inner" else c.inner_door_id + var boundary: String = "unavailable" + for other in doors: + if other.door_id == opposite: boundary = "%s, seal %.0f%%, %s" % ["open" if other.opened else "closed", other.seal_condition * 100.0, "leaking" if other.pressure_room.leaking else "retaining"] + return "%s chamber door; pair %s / %s; habitat %.3f / chamber %.3f / exterior %.6f kPa; opposite %s; valves inner %s outer %s; delta load %.1f / 1000 N" % [c.side, c.inner_door_id.right(8), c.outer_door_id.right(8), c.habitat_pressure_kpa, c.chamber_pressure_kpa, c.exterior_pressure_kpa, boundary, c.inner_bypass_open, c.outer_bypass_open, absf(door.pressure_room.pressure_kpa - door.pressure_room.ambient_pressure_kpa) * 1710.0] if not door.get("pressure_room", {}).is_empty(): var room: Dictionary = door.pressure_room var difference: float = absf(float(room.pressure_kpa) - float(room.ambient_pressure_kpa)) diff --git a/tests/godot/commissioned_room_equalization_test.gd b/tests/godot/commissioned_room_equalization_test.gd index 40369d46..2944d160 100644 --- a/tests/godot/commissioned_room_equalization_test.gd +++ b/tests/godot/commissioned_room_equalization_test.gd @@ -149,7 +149,7 @@ func bypass_reject(operation: String, extra: Dictionary = {}) -> void: func malformed_bypass_restores(snapshot: String) -> void: for mutation in ["bill", "orphan", "missing", "open_type", "extra", "version", "grant", "pressure_legacy"]: - var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(true, false); root.add_child(target) var before: Variant = JSON.parse_string(target.capture_world_snapshot_json().value) var bad: Dictionary = JSON.parse_string(snapshot) match mutation: @@ -184,7 +184,7 @@ func overrating_rejection_controls(snapshot: String) -> void: var shift: float = (target_difference - current) / (factor * (1.0 / float(room_record.volume_m3) + 1.0 / float(ambient_record.volume_m3))) room_record.gas_moles += shift; ambient_record.gas_moles -= shift bad.systems.pressure.room_valves[door_id] = opened - var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(true, false); root.add_child(target) check(target.restore_world_snapshot_json(JSON.stringify(bad)).is_success, "isolated finite overrating rejection fixture restores") var owner: ProductionSimulationSystem = target.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) var gas: PressureSystem = target.get_simulation_system(PressureSystem.SYSTEM_ID) diff --git a/tests/godot/paid_chamber_test.gd b/tests/godot/paid_chamber_test.gd new file mode 100644 index 00000000..74ea815b --- /dev/null +++ b/tests/godot/paid_chamber_test.gd @@ -0,0 +1,394 @@ +extends "res://tests/godot/paid_room_reinforcement_test.gd" + +var inner_id: String +var outer_id: String +var original_spawn: Array +var transit_remote: RemoteWorldRuntime + + +## Real factory and finite resources; no positive-route owner/stock/gas writes. +func run() -> void: + world = FirstPlayableBootstrap.create_playable_world() + root.add_child(world) + world.command_completed.connect(func(_command: SimulationCommand, result: ServiceResult) -> void: last = result) + check(world.start_simulation().is_success, "fresh chamber factory starts") + runner = world.get_node("SimulationRunner"); runner.set_physics_process(false) + production = world.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) + inventory = world.get_simulation_system(InventorySimulationSystem.SYSTEM_ID) + access = world.get_simulation_system(AccessControlSystem.SYSTEM_ID) + context = AuthenticatedCommandContext.for_local_development(&"local-inventory-target", world.world_id, world.region_id) + original_spawn = inventory.own_position(context).value.duplicate() + var pristine: Dictionary = production.capture_snapshot().value + check(pristine.schema_version == 15 and pristine.catalog_revision == 4 and ProductionSimulationSystem._mass(pristine.deposits) == 54000000, "explicit finite 54kg origin") + move([0, 0, 0]) + inventory_action("world", {"operation": "pickup", "item_id": "resource-canister-2", "world_container_id": "world.tray-1", "destination_container_id": "body.human-1", "slot_id": ""}) + inventory_action("self", {"operation": "equip", "item_id": "extractor-2", "source_container_id": "storage.backpack-1", "destination_container_id": "", "slot_id": "hand.right"}) + inventory_action("self", {"operation": "select_hand", "item_id": "", "source_container_id": "", "destination_container_id": "", "slot_id": "hand.right"}) + for batch in range(39): + move([-3000, 0, -1000]) + var source: String = "deposit.iron" if batch < 10 else ("deposit.iron.outcrop" if batch < 17 else ("deposit.iron.reinforcement" if batch < 24 else "deposit.iron.chamber")) + if batch == 24: + var panel := ActorPanel.new(); panel.actor = context + root.add_child(panel); panel.configure(world); panel.set_process(false) + var found: int = -1 + for index in range(panel._deposit.item_count): + if panel._deposit.get_item_text(index) == source: found = index + check(found >= 0, "actual source selector exposes distinct chamber seam") + if found >= 0: panel._deposit.select(found) + panel.refresh(); check(not panel._extract_button.disabled, "held extractor can acquire selected chamber ore") + panel.free() + command("tool_extract", {"deposit_id": source, "carrier_id": "resource-canister-2", "amount_milligrams": 1000000}) + move([0, 0, 0]) + command("unload_cargo", {"carrier_id": "resource-canister-2", "material": "ore.iron", "amount_milligrams": 1000000}) + command("process", {"recipe_id": "refine.iron"}); step(120) + if failures: finish(); return + for batch in range(21): command("process", {"recipe_id": "craft.plate"}); step(120) + command("fabricate_equipment", {"blueprint_id": "equipment.crowbar"}) + inventory_action("self", {"operation": "unequip", "item_id": "", "source_container_id": "", "destination_container_id": "storage.backpack-1", "slot_id": "hand.right"}) + var crowbar: String = "" + for item in inventory.own_inventory_observation(context).value.items: + if item.definition_id == "item.crowbar.fabricated": crowbar = item.item_id + equip(crowbar) + build_chamber() + if failures: finish(); return + var pair: ServiceResult = PaidChamber.candidate(production._state, String(context.principal_id), "body.human-1") + check(pair.is_success, "exact paid two-door topology") + if not pair.is_success: finish(); return + inner_id = pair.value.inner_door_id; outer_id = pair.value.outer_door_id + for id in [inner_id, outer_id]: + door_id = id; door_command("door_seal_install", {"door_id": id}); chamber_ui(8) + door_id = inner_id + var insufficient: Dictionary = production._state.duplicate(true) + insufficient.store.plate = PaidChamber.MASS - 1 + var stock_before: Dictionary = production.capture_snapshot().value + check(not PaidChamber.plan(insufficient, inventory, access, context, "insufficient-chamber-kit", "door_chamber_kit_install", {"expected_revision": stock_before.state_revision, "control_epoch": 0, "door_id": inner_id}, pressure_owner()).is_success and production.capture_snapshot().value == stock_before, "kit cannot consume insufficient detached stock or change live owners") + chamber_ui(14); chamber_ui(16) + if failures: finish(); return + var paid: Dictionary = production.capture_snapshot().value + check(paid.energy_joules == 28250 and paid.store.plate == 600000 and paid.store.iron == 400000 and paid.store.tailings == 15600000, "39kg route accounts for20.4kg installed plate, crowbar and71750J") + for source in ["deposit.iron", "deposit.iron.outcrop", "deposit.iron.reinforcement", "deposit.iron.chamber"]: check(paid.deposits[source] == 0, "source exhausted: " + source) + check(paid.deposits["deposit.ice"] == 5000000 and paid.deposits["deposit.regolith"] == 10000000, "untouched non-ore origin") + var initial_moles: float = all_moles() + var snapshot: String = world.capture_world_snapshot_json().value + var observation: Dictionary = production.observation(context).value + var projection := RemoteProductionProjection.new() + check(projection.accept_record(observation).is_success, "real complete v17 projection admits chamber controls") + projection.free() + chamber_negatives(snapshot) + detached_chamber_controls(snapshot) + legacy_chamber_origins() + check_floor_contract() + if failures: finish(); return + # Genuine local body starts at the original observed spawn and walks the + # actual first-playable preview. Command movement mirrors reached positions. + move([3600, 0, -5500]); move(original_spawn) + await physical_transit() + if failures: finish(); return + reload_playable(world.capture_world_snapshot_json().value, "whole chamber reload after both physical crossings") + check(gas_conserved(initial_moles), "all finite volumes conserve total moles across transit and reload") + var habitat_before: float = pressure_owner().volumes[PressureSystem.PROTOTYPE_HABITAT_ID].pressure_kpa() + for cycle in range(2): + door_id = inner_id; chamber_ui(9); step(600); chamber_ui(10) + check(pressure_owner().room_difference(inner_id) < 0.59, "finite inner equalization permits manual force") + chamber_ui(1); chamber_ui(2) + door_id = outer_id; chamber_ui(9) + var isolated: float = pressure_owner().volumes[PressureSystem.PROTOTYPE_HABITAT_ID].gas_moles + step(600); chamber_ui(10) + check(pressure_owner().volumes[PressureSystem.PROTOTYPE_HABITAT_ID].gas_moles == isolated, "outer vent isolates intact habitat boundary") + check(pressure_owner().room_difference(outer_id) < 0.59, "finite outer equalization permits manual force") + chamber_ui(1); chamber_ui(2) + check(gas_conserved(initial_moles), "both-direction cycle conserves all gas") + check(pressure_owner().volumes[PressureSystem.PROTOTYPE_HABITAT_ID].pressure_kpa() < habitat_before, "repeated cycles deplete finite habitat without refill") + door_id = inner_id; bypass_reject("door_chamber_decommission") + chamber_ui(9) + reload_playable(world.capture_world_snapshot_json().value, "open inner bypass resume") + check(pressure_owner().room_valve_open(inner_id), "open paired bypass survives reload") + chamber_ui(10) + var fixture := FileAccess.open("res://var/paid-chamber-fixture.json", FileAccess.WRITE) + fixture.store_string(JSON.stringify({"world": JSON.parse_string(snapshot), "observation": observation, "pristine": pristine})); fixture.close() + finish() + + +func build_chamber() -> void: + for count in range(4): command("materialize_plate", {}) + var plates: Array = production.observation(context).value.components.duplicate() + check(plates.size() == 4, "four actually acquired physical wall plates") + if plates.size() != 4: return + move([6000, 0, -5000]) + for index in range(2): command("wall_build", {"item_id": plates[index], "position": [5500 + index * 1000, 0, -4550], "quarter_turn": 0}) + move([8000, 0, -5500]); move([8000, 0, -2500]); move([6000, 0, -2500]) + for index in range(2): command("wall_build", {"item_id": plates[index + 2], "position": [5500 + index * 1000, 0, -3450], "quarter_turn": 0}) + move([8000, 0, -2500]); move([8000, 0, -4000]) + for bounds in [[6100, 7000, -4500, -3500], [7000, 7900, -4500, -3500]]: command("panel_build", {"kind": "floor", "bounds_millimetres": bounds, "height_millimetres": 0}) + for bounds in [[6100, 6900, -4500, -3500]]: command("panel_build", {"kind": "ceiling", "bounds_millimetres": bounds, "height_millimetres": 2000}) + move([8000, 0, -5500]); move([6000, 0, -5000]) + command("panel_build", {"kind": "floor", "bounds_millimetres": [5100, 6100, -4500, -3500], "height_millimetres": 0}) + command("panel_build", {"kind": "ceiling", "bounds_millimetres": [5100, 6100, -4500, -3500], "height_millimetres": 2000}) + move([3600, 0, -5500]); move([3600, 0, -4000]) + command("panel_build", {"kind": "floor", "bounds_millimetres": [4100, 5100, -4500, -3500], "height_millimetres": 0}) + door_command("door_build", {"position": [5050, 0, -4000], "quarter_turn": 1}) + move([3600, 0, -5500]); move([8000, 0, -5500]); move([8000, 0, -4000]) + door_command("door_build", {"position": [6950, 0, -4000], "quarter_turn": 1}) + move([8000, 0, -5500]); move([6000, 0, -5000]) + + +func chamber_ui(index: int, accepted: bool = true) -> void: + var controls := PaidDoorControls.new() + root.add_child(controls); controls.configure(world, context.principal_id); controls.set_process(false) + controls._process(0.2) + for option in range(controls._doors.item_count): + if controls._doors.get_item_metadata(option) == door_id: controls._doors.item_selected.emit(option) + controls._process(0.2) + check(not controls._buttons[index].disabled, "actual selected door action enabled: %d" % index) + controls._buttons[index].pressed.emit() + check(not controls._pending.is_empty(), "actual chamber UI queues authenticated intent") + step(); check(last.is_success == accepted, "chamber UI%d %s: %s" % [index, door_id, last.message]) + controls.free() + + +func check_floor_contract() -> void: + var full: Array[Dictionary] = [] + var short: Array[Dictionary] = [] + for id in production._state.panels: + var panel: Dictionary = production._state.panels[id] + full.append(HorizontalPanelConstruction.descriptor(id, panel)) + if panel.bounds_millimetres[0] < 7000: short.append(HorizontalPanelConstruction.descriptor(id, panel)) + for x in [4500, 5050, 5550, 6950, 7500]: + var query: Dictionary = {"schema_version": 1, "expected_production_revision": 0, "footprint_millimetres": [x - 300, x + 300, -4300, -3700], "foot_height_millimetres": 0, "vertical_tolerance_millimetres": 10} + check(HorizontalFloorSupportQuery.query(query, full, 0).value.supported, "full600mm footprint supported across both apertures:%d" % x) + if x == 6950: check(not HorizontalFloorSupportQuery.query(query, short, 0).value.supported, "old short approach fails at outer aperture") + + +func physical_transit() -> void: + host = RecordingHost.new(); root.add_child(host); host.set_process(false); host._world = world + host._keys = ["chamber-actor-000000000000000000", "chamber-target-00000000000000000"] + world.command_completed.connect(host._on_completed); connect_door_peer(1, 1) + transit_remote = RemoteWorldRuntime.new(); root.add_child(transit_remote); transit_remote.set_process(false) + transit_remote._receive({"type": "authenticated", "slot": 1, "world_id": String(world.world_id), "region_id": String(world.region_id)}) + var preview := ProceduralHabitatPreview.new(); root.add_child(preview) + check(not preview._paid_worksite_legacy_collider.disabled, "normal preview retains its legacy wall collider") + preview.focus_production() + preview.production.observe(production.observation(context).value) + check(preview._paid_worksite_legacy_collider.disabled and not preview._paid_worksite_legacy_wall.visible, "production focus removes only exact obsolete worksite obstruction") + var doors := PaidDoorContactScene.new(); root.add_child(doors); doors.bind(transit_remote); doors.set_physics_process(false) + var panels := PaidPanelContactScene.new(); root.add_child(panels); panels.bind(transit_remote); panels.set_physics_process(false) + transit_remote._receive(publish_door(1)) + var actor: LocalHabitatCharacter = preview.character + actor.position = ProceduralWorldInventoryPreview.DISPLAY_ORIGIN + Vector3(float(original_spawn[0]), maxf(950.0, float(original_spawn[1])), float(original_spawn[2])) / 1000.0 + actor.rotation.y = 0.0; actor.read_local_keyboard = false; actor.set_navigation_enabled(true); actor.set_physics_process(false) + await walk_to(actor, Vector2(-2.4, -3.5)) + await walk_to(actor, Vector2(-1.5, -3.5)) + await walk_to(actor, Vector2(-1.5, -2.0)) + check(actor.is_on_floor(), "actual spawn-to-inner-approach route remains grounded") + var closed_start: Vector3 = actor.position + actor.set_motion_intent(Vector2.RIGHT, false) + for frame in range(12): await physics_frame; actor._physics_process(1.0 / 60.0) + check(actor.position.x < -1.25, "paid closed inner leaf blocks actual body after preview cleanup") + actor.set_motion_intent(Vector2.ZERO, false) + # Submit observed reached positions, never reposition the body after spawn. + await walk_to(actor, Vector2(-1.5, -3.0)) + await walk_to(actor, Vector2(-0.95, -3.0)); bridge_body(actor) + door_id = inner_id; chamber_ui(1) + transit_remote._receive(publish_door(1)); await physics_frame + await walk_to(actor, Vector2(-1.5, -3.0)) + await walk_to(actor, Vector2(-1.5, -2.0)) + await walk_to(actor, Vector2(-0.45, -2.0)); bridge_body(actor); chamber_ui(2) + check(actor.position.distance_to(closed_start) > 0.5 and actor.is_on_floor(), "actual600mm body crosses inner aperture onto chamber floor") + door_id = outer_id; chamber_ui(9); step(600); chamber_ui(10); chamber_ui(1) + transit_remote._receive(publish_door(1)); await physics_frame + await walk_to(actor, Vector2(1.5, -2.0)); bridge_body(actor); chamber_ui(2) + check(actor.is_on_floor(), "actual body fully crosses outer aperture onto extended paid floor") + # Return in the opposite direction with the same finite hardware/gas. + chamber_ui(1); transit_remote._receive(publish_door(1)); await physics_frame + await walk_to(actor, Vector2(-0.45, -2.0)); bridge_body(actor); chamber_ui(2) + door_id = inner_id; chamber_ui(9); step(600); chamber_ui(10); chamber_ui(1) + transit_remote._receive(publish_door(1)); await physics_frame + await walk_to(actor, Vector2(-1.5, -2.0)) + await walk_to(actor, Vector2(-1.5, -3.0)) + await walk_to(actor, Vector2(-0.95, -3.0)); bridge_body(actor); chamber_ui(2) + check(actor.is_on_floor(), "actual body returns through both supported apertures") + # Move outside both sweeps for later isolated pressure cycles. + await walk_to(actor, Vector2(-1.5, -3.5)); bridge_body(actor) + await walk_to(actor, Vector2(0.0, -3.0)); bridge_body(actor) + panels.free(); doors.free(); transit_remote.free(); preview.free(); host.free() + + +func walk_to(actor: LocalHabitatCharacter, destination: Vector2) -> void: + for frame in range(150): + var offset: Vector2 = destination - Vector2(actor.position.x, actor.position.z) + if offset.length() < 0.04: + actor.set_motion_intent(Vector2.ZERO, false) + bridge_body(actor) + return + actor.set_motion_intent(offset.normalized() * minf(1.0, offset.length() * 10.0), false) + await physics_frame + actor._physics_process(1.0 / 60.0) + actor.set_motion_intent(Vector2.ZERO, false) + check(false, "actual body reached continuous walking destination: %s, actual:%s" % [destination, actor.position]) + + +func bridge_body(actor: LocalHabitatCharacter) -> void: + var point: Vector3 = (actor.position - ProceduralWorldInventoryPreview.DISPLAY_ORIGIN) * 1000.0 + move([roundi(point.x), roundi(point.y), roundi(point.z)]) + + +func chamber_negatives(snapshot: String) -> void: + door_id = inner_id + for operation in ["door_chamber_kit_install", "door_chamber_kit_recover", "door_chamber_commission", "door_chamber_decommission", "door_remove", "door_seal_remove", "door_reinforcement_install", "door_bypass_recover"]: bypass_reject(operation) + bypass_reject("door_bypass_open", {"control_epoch": 1}) + bypass_reject("door_bypass_open", {"expected_revision": production._state.state_revision - 1}) + bypass_reject("door_chamber_commission", {"gas_moles": 1}) + var own: AuthenticatedCommandContext = context + bypass_reject("door_chamber_decommission", {"control_epoch": 1}) + var retained: Dictionary = production._state.receipts.back() + var retry: Dictionary = {"expected_revision": int(retained.state_revision) - 1, "control_epoch": 0, "door_id": inner_id} + var unchanged: Dictionary = production.capture_snapshot().value + check(production.apply_local(context, retained.request_id, "door_chamber_commission", retry, runner.tick_number + 1).is_success and production.capture_snapshot().value == unchanged, "exact chamber receipt replay cannot allocate twice") + retry.door_id = outer_id + check(not production.apply_local(context, retained.request_id, "door_chamber_commission", retry, runner.tick_number + 1).is_success and production.capture_snapshot().value == unchanged, "changed chamber replay cannot redirect paired intent") + context = AuthenticatedCommandContext.for_local_development(&"local-inventory-actor", world.world_id, world.region_id) + bypass_reject("door_bypass_open"); context = own + door_id = outer_id; bypass_reject("door_open") + # The legacy synchronization hook may never bypass paid admission. + access.update_actuator_position(StringName(outer_id), PaidDoorConstruction.actuator_id(outer_id), true) + check(access.devices[StringName(outer_id)].actuator(PaidDoorConstruction.actuator_id(outer_id)).position == 0.0, "legacy low-level synchronization cannot open paid leaf") + for mutation in ["bill", "endpoint", "policy", "half_kit", "half_valve", "orphan", "both_doors", "both_valves", "door_valve", "area", "owner", "float_bill", "extra"]: + var bad: Dictionary = JSON.parse_string(snapshot) + match mutation: + "bill": bad.systems.production.chamber_kits[inner_id].materials.plate += 1 + "float_bill": bad.systems.production.chamber_kits[inner_id].materials.plate = 6000000.5 + "endpoint": bad.systems.pressure.paid_chamber.outer_endpoint = "habitat_main" + "policy": bad.systems.pressure.paid_chamber.policy_id = ConstructedRoomPressure.POLICY + "half_kit": bad.systems.production.chamber_kits.clear(); bad.systems.production.store.plate += PaidChamber.MASS + "half_valve": bad.systems.pressure.room_valves.erase(outer_id) + "orphan": bad.systems.pressure.paid_chamber.volume_id = "paid-chamber-orphan" + "both_valves": bad.systems.pressure.room_valves[inner_id] = true; bad.systems.pressure.room_valves[outer_id] = true + "owner": bad.systems.pressure.paid_chamber.body_id = "body.human-2" + "extra": bad.systems.pressure.paid_chamber.retention_kpa = 1000 + _: + for device in bad.systems.access_control.devices: + if device.device_id in [inner_id, outer_id]: + for actuator_record in device.actuators: + if mutation == "area": actuator_record.effective_pressure_area_m2 = 0.0 + elif mutation == "both_doors" or device.device_id == inner_id: actuator_record.position = 1.0 + if mutation == "door_valve": bad.systems.pressure.room_valves[outer_id] = true + var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + var before: String = target.capture_world_snapshot_json().value + check(not target.restore_world_snapshot_json(JSON.stringify(bad)).is_success, "malformed atomic paired restore rejects:" + mutation) + check(target.capture_world_snapshot_json().value == before, "malformed paired restore preserves every owner/tick/epoch:" + mutation) + target.free() + door_id = inner_id + + +## Deliberately altered detached fixtures never fund the positive route. +func detached_chamber_controls(snapshot: String) -> void: + var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + check(target.restore_world_snapshot_json(snapshot).is_success, "isolated paired controls restore") + var stock: ProductionSimulationSystem = target.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) + var items: InventorySimulationSystem = target.get_simulation_system(InventorySimulationSystem.SYSTEM_ID) + var hardware: AccessControlSystem = target.get_simulation_system(AccessControlSystem.SYSTEM_ID) + var gas: PressureSystem = target.get_simulation_system(PressureSystem.SYSTEM_ID) + var actor := AuthenticatedCommandContext.for_local_development(context.principal_id, target.world_id, target.region_id) + # Reach uses trusted bound body pose, with identical legacy/real center semantics. + for height in [0, 900]: + items._bodies["body.human-1"].position_millimetres = [5450, height, -4000] + check(PaidDoorConstruction.reachable(items, actor, [6950, 0, -4000]).is_success, "handle at exact1500mm accepted for y%d" % height) + items._bodies["body.human-1"].position_millimetres = [5449, height, -4000] + check(not PaidDoorConstruction.reachable(items, actor, [6950, 0, -4000]).is_success, "handle beyond1500mm rejected for y%d" % height) + for principal in [&"unbound", &"local-inventory-actor"]: + var foreign := AuthenticatedCommandContext.for_local_development(principal, target.world_id, target.region_id) + check(not stock._admit_paid_open(inner_id, foreign).is_success, "foreign/unbound cannot borrow paid handle") + var wrong_scope := AuthenticatedCommandContext.for_local_development(context.principal_id, &"wrong-world", target.region_id) + check(not PaidDoorConstruction.reachable(items, wrong_scope, [6950, 0, -4000]).is_success, "handle rejects wrong world session") + items._bodies["body.human-1"].position_millimetres = [6000, 900, -5000] + var inner: AccessControlledDeviceState = hardware.devices[StringName(inner_id)] + var outer: AccessControlledDeviceState = hardware.devices[StringName(outer_id)] + var prepared := PreparedDoorChange.new(hardware, StringName(inner_id), inner, inner.to_record(), false) + check(prepared.stage_device(StringName(outer_id), outer.to_record()).is_success, "stage second device without writes") + outer.actuator(PaidDoorConstruction.actuator_id(outer_id)).seal_condition = 0.5 + var before: Dictionary = hardware.capture_snapshot().value + check(not prepared.commit(hardware).is_success and hardware.capture_snapshot().value == before, "stale second CAS leaves both devices untouched") + check(not prepared.commit(hardware).is_success, "failed paired commit remains consumed") + check(not gas.validate_room_open(inner_id).is_success, "damaged opposite blocks opening") + inner.actuator(PaidDoorConstruction.actuator_id(inner_id)).position = 1.0 + check(hardware.validate_door_actuation(StringName(inner_id), PaidDoorConstruction.actuator_id(inner_id), false, 0.0, true, actor).is_success, "damaged opposite never prevents own force-safe closing") + target.free() + for entry in [[0.6, "intact"], [-0.6, "intact"], [110.1, "intact"], [0.6, "integrity"], [-0.6, "seal_condition"]]: + var bad: Dictionary = JSON.parse_string(snapshot) + var room: Dictionary = {}; var habitat: Dictionary = {} + for volume in bad.systems.pressure.volumes: + if volume.volume_id == bad.systems.pressure.paid_chamber.volume_id: room = volume + if volume.volume_id == "habitat_main": habitat = volume + var factor: float = PressureVolumeState.GAS_CONSTANT_J_PER_MOL_K * float(room.temperature_kelvin) / 1000.0 + var current: float = factor * (float(room.gas_moles) / float(room.volume_m3) - float(habitat.gas_moles) / float(habitat.volume_m3)) + var shift: float = (float(entry[0]) - current) / (factor * (1.0 / float(room.volume_m3) + 1.0 / float(habitat.volume_m3))) + room.gas_moles += shift; habitat.gas_moles -= shift + for device in bad.systems.access_control.devices: + if device.device_id == inner_id and entry[1] != "intact": device.actuators[0][entry[1]] = 0.5 + target = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + check(target.restore_world_snapshot_json(JSON.stringify(bad)).is_success, "isolated finite chamber load restores") + stock = target.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) + gas = target.get_simulation_system(PressureSystem.SYSTEM_ID) + actor = AuthenticatedCommandContext.for_local_development(context.principal_id, target.world_id, target.region_id) + before = stock.capture_snapshot().value + var rejected: ServiceResult = stock.apply_local(actor, "chamber-force-negative", "door_open", {"expected_revision": before.state_revision, "control_epoch": 0, "door_id": inner_id}, int(bad.tick_number) + 1) + check(not rejected.is_success and rejected.error_code == &"actuator_force_exceeded" and stock.capture_snapshot().value == before, "both pressure signs preserve1000N manual limit") + var leaking: bool = gas.room_observation(inner_id).leaking + check(leaking == (absf(float(entry[0])) > 110.0 or entry[1] != "intact"), "chamber rating and damage independently govern retention") + var moles_before: float = gas.volumes[StringName(gas.paid_chamber.volume_id)].gas_moles + gas._step_paid_chamber(1.0 / 60.0) + check((gas.volumes[StringName(gas.paid_chamber.volume_id)].gas_moles != moles_before) == leaking, "closed valves do not suppress boundary leak") + if leaking: + var closed_result: Dictionary = gas.capture_snapshot().value + check(target.restore_world_snapshot_json(JSON.stringify(bad)).is_success, "reset same finite leak control") + gas.room_valves[inner_id] = true + gas._step_paid_chamber(1.0 / 60.0) + check(gas.capture_snapshot().value.volumes == closed_result.volumes, "open valve plus leak uses one conductance") + target.free() + # A recovery-positive fixture deliberately equalizes all three finite endpoints. + # It demonstrates the documented expensive precondition, never a vacuum refill. + var low: Dictionary = JSON.parse_string(snapshot) + var endpoints: Array = [] + var total_moles: float = 0.0; var total_volume: float = 0.0 + for volume in low.systems.pressure.volumes: + if volume.volume_id in ["habitat_main", "exterior_reservoir", low.systems.pressure.paid_chamber.volume_id]: + endpoints.append(volume); total_moles += float(volume.gas_moles); total_volume += float(volume.volume_m3) + for volume in endpoints: volume.gas_moles = total_moles * float(volume.volume_m3) / total_volume + target = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + check(target.restore_world_snapshot_json(JSON.stringify(low)).is_success, "finite force-safe decommission fixture restores") + stock = target.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) + gas = target.get_simulation_system(PressureSystem.SYSTEM_ID) + hardware = target.get_simulation_system(AccessControlSystem.SYSTEM_ID) + actor = AuthenticatedCommandContext.for_local_development(context.principal_id, target.world_id, target.region_id) + var seals: Dictionary = hardware.capture_snapshot().value + var gas_before: float = 0.0 + for volume in gas.volumes.values(): gas_before += volume.gas_moles + for operation in ["door_chamber_decommission", "door_chamber_kit_recover", "door_chamber_kit_install", "door_chamber_commission"]: + if operation == "door_chamber_kit_recover": + for reserved in [false, true]: + var full: Dictionary = stock._state.duplicate(true) + if reserved: full.job = {"outputs": {"plate": ProductionSimulationSystem.MAX_MASS}} + else: full.store.tailings = ProductionSimulationSystem.MAX_MASS + check(not PaidChamber.plan(full, target.get_simulation_system(InventorySimulationSystem.SYSTEM_ID), hardware, actor, "full-chamber-recovery", operation, {"expected_revision": full.state_revision, "control_epoch": 0, "door_id": inner_id}, gas).is_success, "recovery respects stock and reserved-output capacity") + var damaged: ActuatorState = hardware.devices[StringName(outer_id)].actuator(PaidDoorConstruction.actuator_id(outer_id)) + damaged.integrity = 0.5 + var detached: Dictionary = stock._state.duplicate(true) + check(not PaidChamber.plan(detached, target.get_simulation_system(InventorySimulationSystem.SYSTEM_ID), hardware, actor, "damaged-chamber-recovery", operation, {"expected_revision": detached.state_revision, "control_epoch": 0, "door_id": inner_id}, gas).is_success and damaged.integrity == 0.5, "recovery never heals a damaged original actuator") + damaged.integrity = 1.0 + var result: ServiceResult = stock.apply_local(actor, "isolated-" + operation, operation, {"expected_revision": stock._state.state_revision, "control_epoch": 0, "door_id": inner_id}, int(low.tick_number) + 1) + check(result.is_success, "force-safe recovery/refit: " + operation + ": " + result.message) + check(stock._state.store.plate == 600000 and hardware.capture_snapshot().value == seals, "recover/refit conserves plate and original Access condition") + var gas_after: float = 0.0 + for volume in gas.volumes.values(): gas_after += volume.gas_moles + check(absf(gas_after - gas_before) < 1e-8, "decommission/refit never refills gas") + target.free() + + +func legacy_chamber_origins() -> void: + for mode in [false, true]: + var legacy: WorldRuntime = FirstPlayableBootstrap.create_playable_world(mode, false); root.add_child(legacy) + var owner: ProductionSimulationSystem = legacy.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) + var before: Dictionary = owner.capture_snapshot().value + check(before.catalog_revision == (3 if mode else 2) and not before.deposits.has("deposit.iron.chamber"), "explicit old factory receives no chamber source") + check(legacy.restore_world_snapshot_json(legacy.capture_world_snapshot_json().value).is_success and owner.capture_snapshot().value == before, "legacy reload never grants chamber ore") + legacy.free() diff --git a/tests/godot/paid_room_reinforcement_test.gd b/tests/godot/paid_room_reinforcement_test.gd index 3887a306..f32049c5 100644 --- a/tests/godot/paid_room_reinforcement_test.gd +++ b/tests/godot/paid_room_reinforcement_test.gd @@ -7,7 +7,7 @@ class ActorPanel extends LocalProductionPanel: ## Real playable factory, finite acquisition and actual UI command queue. func run() -> void: - world = FirstPlayableBootstrap.create_playable_world() + world = FirstPlayableBootstrap.create_playable_world(true, false) root.add_child(world) world.command_completed.connect(func(_command: SimulationCommand, result: ServiceResult) -> void: last = result) check(world.start_simulation().is_success, "new playable reinforcement world starts") @@ -148,7 +148,7 @@ func reinforcement_negative_controls(snapshot: String) -> void: if device.device_id == door_id: device.actuators[0].effective_pressure_area_m2 = 0.0 "grant": bad.systems.production.store.plate += PaidRoomReinforcement.MASS "extra": bad.systems.production.room_reinforcements[door_id].health = 1.0 - var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(true, false); root.add_child(target) var before: Variant = JSON.parse_string(target.capture_world_snapshot_json().value) check(not target.restore_world_snapshot_json(JSON.stringify(bad)).is_success, "cross-owner malformed restore rejects: " + mutation) check(same_json_value(before, JSON.parse_string(target.capture_world_snapshot_json().value)), "malformed restore preserves all owners: " + mutation) @@ -166,7 +166,7 @@ func reinforcement_negative_controls(snapshot: String) -> void: room_record.gas_moles += shift; ambient_record.gas_moles -= shift for device in bad.systems.access_control.devices: if device.device_id == door_id and entry[1] != "intact": device.actuators[0][entry[1]] = 0.5 - var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(true, false); root.add_child(target) check(target.restore_world_snapshot_json(JSON.stringify(bad)).is_success, "finite isolated retention control restores") var gas: PressureSystem = target.get_simulation_system(PressureSystem.SYSTEM_ID) var owner: ProductionSimulationSystem = target.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) diff --git a/tests/schema/paid_chamber_schema_test.py b/tests/schema/paid_chamber_schema_test.py new file mode 100644 index 00000000..4f6ab316 --- /dev/null +++ b/tests/schema/paid_chamber_schema_test.py @@ -0,0 +1,72 @@ +"""Versioned chamber schemas; CI requires genuine emitted runtime fixtures.""" +from copy import deepcopy +import json +from pathlib import Path +import sys + +from jsonschema import Draft202012Validator +from referencing import Registry, Resource + +ROOT = Path(__file__).resolve().parents[2] +paths = list((ROOT / "docs/schema").glob("*.schema.json")) +schemas = [json.loads(path.read_text(encoding="utf-8-sig")) for path in paths] +registry = Registry().with_resources( + [(s["$id"], Resource.from_contents(s)) for s in schemas if "$id" in s] + + [(p.name, Resource.from_contents(s)) for p, s in zip(paths, schemas)] +) + + +def validator(name): + schema = next(s for s in schemas if s.get("$id") == "urn:frontier-systems:schema:" + name) + Draft202012Validator.check_schema(schema) + return Draft202012Validator(schema, registry=registry) + + +for name in ["production-system:v15", "production-observation:v17", "paid-door-observation:v6", "pressure-system:v5", "world-snapshot:v1"]: + validator(name) +intent = validator("production-intent:v1") +for operation in ["door_chamber_kit_install", "door_chamber_kit_recover", "door_chamber_commission", "door_chamber_decommission"]: + value = {"operation": operation, "payload": {"expected_revision": 0, "control_epoch": 0, "door_id": "door-test"}} + intent.validate(value) + for field in ["health", "seal_condition", "integrity", "retention_kpa", "materials", "principal_id", "endpoint", "gas_moles"]: + bad = deepcopy(value) + bad["payload"][field] = 1 + assert not intent.is_valid(bad), (operation, field) + +if "--schema-only" in sys.argv: + print("Chamber schema definitions and strict intents passed; emitted runtime fixture not checked") + sys.exit(0) + +fixture = json.loads((ROOT / "var/paid-chamber-fixture.json").read_text(encoding="utf-8")) +world = fixture["world"] +production = world["systems"]["production"] +pressure = world["systems"]["pressure"] +observation = fixture["observation"] +validator("world-snapshot:v1").validate(world) +validator("production-system:v15").validate(production) +validator("production-system:v15").validate(fixture["pristine"]) +validator("pressure-system:v5").validate(pressure) +validator("production-observation:v17").validate(observation) +assert production["catalog_revision"] == 4 +assert sum(fixture["pristine"]["deposits"].values()) == 54000000 +assert production["energy_joules"] == 28250 +assert production["store"]["plate"] == 600000 +assert production["store"]["iron"] == 400000 +assert production["store"]["tailings"] == 15600000 +for field, value in [("materials", {"plate": 6000001}), ("policy_id", "client-rating"), ("health", 1)]: + bad = deepcopy(production) + next(iter(bad["chamber_kits"].values()))[field] = value + assert not validator("production-system:v15").is_valid(bad), field +for field, value in [("outer_endpoint", "habitat_main"), ("policy_id", "client-rating"), ("retention_kpa", 999)]: + bad = deepcopy(pressure) + bad["paid_chamber"][field] = value + assert not validator("pressure-system:v5").is_valid(bad), field +for field in ["chamber_kit_installed", "reinforcement_intact"]: + bad = deepcopy(observation) + bad["paid_doors"]["doors"][0][field] = 1 + assert not validator("production-observation:v17").is_valid(bad), field +for legacy_version in [11, 12, 13, 14]: + bad = deepcopy(production) + bad["schema_version"] = legacy_version + assert not validator(f"production-system:v{legacy_version}").is_valid(bad) +print("Chamber genuine emitted finite budget, persistence, observation and negative schemas passed") From eaf77177fda45b495f1b9e82027cd5177716472e Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:50:59 -0400 Subject: [PATCH 04/11] Resolve chamber sources from pinned catalogs and support observation17 controls --- docs/architecture/production-gameplay-loop.md | 9 ++++ src/core/machinery/machinery_construction.gd | 2 +- .../production_simulation_system.gd | 32 +++++++++---- .../production/horizontal_panel_controls.gd | 2 +- .../production/horizontal_panel_preview.gd | 2 +- .../production/local_production_panel.gd | 3 +- .../production/timed_canister_controls.gd | 2 +- .../commissioned_room_equalization_test.gd | 2 +- tests/godot/paid_chamber_test.gd | 46 +++++++++++++++++++ 9 files changed, 83 insertions(+), 17 deletions(-) diff --git a/docs/architecture/production-gameplay-loop.md b/docs/architecture/production-gameplay-loop.md index 71f89921..e6537fb2 100644 --- a/docs/architecture/production-gameplay-loop.md +++ b/docs/architecture/production-gameplay-loop.md @@ -322,3 +322,12 @@ bounded simulation counters remain available; no per-frame diagnostic tracing or external service is added. See the [chamber milestone](../development/paid-chamber-sequential-interlocks-milestone.md) for pinned geometry, accepted physical corrections and verification scope. + +Source selectors derive choices from the current validated observation, rather +than a hard-coded latest catalog. Scheduled mining, single and timed tool work, +completion and machinery metadata use the candidate's pinned catalog. The +server-only `machine_recipe(id, source_state={})` may receive a detached restore +candidate; omitting it uses live state. This prevents restore validation from +consulting the fresh factory's different catalog and does not grant absent ore. +Timed construction, panel controls and panel previews accept observation17 and +continue rejecting unsupported18+. Their old schema paths remain supported. diff --git a/src/core/machinery/machinery_construction.gd b/src/core/machinery/machinery_construction.gd index 72476729..69096e18 100644 --- a/src/core/machinery/machinery_construction.gd +++ b/src/core/machinery/machinery_construction.gd @@ -167,7 +167,7 @@ static func plan(production: ProductionSimulationSystem, state: Dictionary, inve workshop["placements"][machine]["enabled"] = payload["enabled"] elif operation == "machine_run": var recipe: String = String(payload["recipe"]) - var spec: Dictionary = production.machine_recipe(recipe) + var spec: Dictionary = production.machine_recipe(recipe, state) if spec.is_empty() or workshop["jobs"].has(machine) or not MachineryWorkshop.operational(workshop, owner, machine, spec["capability"]): return failure("Select a compatible recipe on an operational idle machine.") if not spec["deposit"].is_empty() and InventorySpatialMath.distance_millimetres(workshop["placements"][machine]["position"], ProductionSimulationSystem.SOURCE_POSITIONS[spec["deposit"]]) > 1500: return failure("The extractor must be deployed near its resource source.") workshop["jobs"][machine] = {"recipe": recipe, "progress": 0, "energy_spent": 0, "paused": false} diff --git a/src/core/production/production_simulation_system.gd b/src/core/production/production_simulation_system.gd index f42798ce..0f9ccb7f 100644 --- a/src/core/production/production_simulation_system.gd +++ b/src/core/production/production_simulation_system.gd @@ -197,6 +197,16 @@ func _initial_deposits(new_world_outcrop: bool = false, new_world_reinforcement: return result +## Resolve source metadata from the candidate's pinned catalog, never latest. +func _source_catalog(candidate: Dictionary) -> Dictionary: + match int(candidate.catalog_revision): + 1: return _legacy_catalog + 2: return _catalog + 3: return _reinforced_catalog + 4: return _chamber_catalog + return {} + + func _empty_stock() -> Dictionary: var result: Dictionary = {} for id in _catalog["materials"]: @@ -615,14 +625,14 @@ func _prepare_tool_extraction(candidate: Dictionary, context: AuthenticatedComma candidate["job"]["extraction"]["status"] = "running" candidate["job"]["extraction"]["pause_reason"] = "" return ServiceResult.success() - if _tools == null or not _fields(payload, ["expected_revision", "deposit_id", "carrier_id", "cycles"]) or not candidate["deposits"].has(payload.get("deposit_id")) or not ControlContractValidation.is_id(payload.get("carrier_id")) or not ControlContractValidation.is_integer(payload.get("cycles"), 1, ToolExtractionWork.MAX_CYCLES) or not candidate["job"].is_empty() or int(candidate["state_revision"]) > MAX_REVISION - 100: return _failure("Reserve 1–10 extraction cycles with exact source/carrier identities while production is idle.") + if _tools == null or not _fields(payload, ["expected_revision", "deposit_id", "carrier_id", "cycles"]) or not candidate["deposits"].has(payload.get("deposit_id")) or not ControlContractValidation.is_id(payload.get("carrier_id")) or not ControlContractValidation.is_integer(payload.get("cycles"), 1, ToolExtractionWork.MAX_CYCLES) or not candidate["job"].is_empty() or int(candidate["state_revision"]) > MAX_REVISION - 100: return _failure("Reserve 1-10 extraction cycles with exact source/carrier identities while production is idle.") var active: ServiceResult = _inventory.active_tool(context, "tool.extractor") if not active.is_success: return active var binding: Dictionary = {"request_id": request_id, "principal_id": String(context.principal_id), "tool_id": active.value, "carrier_id": payload["carrier_id"], "deposit_id": payload["deposit_id"], "remaining_cycles": int(payload["cycles"]), "completed_cycles": 0, "status": "running", "pause_reason": ""} if _tool_extraction_reason(binding, context) != "": return _failure("The bound tool, carrier, power and energy must be available.") var mass: int = int(payload["cycles"]) * ToolExtractionWork.CYCLE_MASS if int(candidate["deposits"][payload["deposit_id"]]) < mass: return _failure("The source cannot satisfy this reservation.") - var material: String = _reinforced_catalog["deposits"][payload["deposit_id"]]["material"] + var material: String = _source_catalog(candidate)["deposits"][payload["deposit_id"]]["material"] candidate["deposits"][payload["deposit_id"]] -= mass candidate["job"] = {"recipe_id": "", "deposit_id": payload["deposit_id"], "station": "drill", "inputs": {material: mass}, "outputs": {material: mass}, "remaining_ticks": ToolExtractionWork.CYCLE_TICKS, "extraction": binding} return ServiceResult.success() @@ -663,7 +673,7 @@ func _simulate_tool_extraction(tick: int) -> void: if not usage.is_success: _pause_tool_extraction("capability") return - var material: String = _reinforced_catalog["deposits"][binding["deposit_id"]]["material"] + var material: String = _source_catalog(candidate)["deposits"][binding["deposit_id"]]["material"] var completed: int = int(binding["completed_cycles"]) + 1 if int(binding["remaining_cycles"]) == 1: candidate["job"] = {} @@ -751,7 +761,7 @@ func _plan(candidate: Dictionary, operation: String, payload: Dictionary) -> Ser var energy: int = maxi(1, int(ceil(float(amount) / 4000.0))) if int(candidate["deposits"][payload["deposit_id"]]) < amount or int(candidate["energy_joules"]) < energy or not candidate["powered"]["drill"]: return _failure("Source mass, extractor power or finite energy is unavailable.") - var material: String = _reinforced_catalog["deposits"][payload["deposit_id"]]["material"] + var material: String = _source_catalog(candidate)["deposits"][payload["deposit_id"]]["material"] candidate["cargo"][id][material] += amount if not _cargo_valid(candidate["cargo"][id]): return _failure("The receiving canister is full, incompatible or overpressure.") @@ -837,7 +847,7 @@ func _plan(candidate: Dictionary, operation: String, payload: Dictionary) -> Ser deposit_id = payload["deposit_id"] if int(candidate["deposits"][deposit_id]) < 1000000: return _failure("This deposit is exhausted.") - inputs = {_reinforced_catalog["deposits"][deposit_id]["material"]: 1000000} + inputs = {_source_catalog(candidate)["deposits"][deposit_id]["material"]: 1000000} outputs = inputs.duplicate(true) station = "drill" duration = 60 @@ -899,11 +909,11 @@ func capture_snapshot() -> ServiceResult: ## Work recipes share the trusted refinery/chemistry recipes and finite deposits. -func machine_recipe(id: String) -> Dictionary: +func machine_recipe(id: String, source_state: Dictionary = {}) -> Dictionary: if id == "chemical.electrolysis.micro": # Portable 2 L carriers cannot receive the bulk one-kilogram gas batch. # A separate exact 10 g recipe preserves old recipe/save semantics. - var micro: Dictionary = machine_recipe("chemical.electrolysis") + var micro: Dictionary = machine_recipe("chemical.electrolysis", source_state) for stock in ["inputs", "outputs"]: for material in micro[stock]: micro[stock][material] = int(micro[stock][material]) / 100 micro["energy_joules"] = int(micro["energy_joules"]) / 100 @@ -915,8 +925,10 @@ func machine_recipe(id: String) -> Dictionary: return recipe if id.begins_with("extract."): var deposit: String = "deposit.%s" % id.trim_prefix("extract.") - if _chamber_catalog["deposits"].has(deposit): - var material: String = _chamber_catalog["deposits"][deposit]["material"] + var candidate: Dictionary = _state if source_state.is_empty() else source_state + var sources: Dictionary = _source_catalog(candidate)["deposits"] + if candidate.deposits.has(deposit) and sources.has(deposit): + var material: String = sources[deposit]["material"] return {"inputs": {}, "outputs": {material: 1000000}, "duration_ticks": 60, "energy_joules": 1000, "capability": "extractor", "deposit": deposit} return {} @@ -1129,7 +1141,7 @@ func _decode(record: Dictionary, tick: int) -> ServiceResult: if not MachineryWorkshop.validate(normalized["machinery"]): return _failure("Malformed machinery record.") normalized["machinery"]["catalog_fingerprint"] = MachineryWorkshop.initial()["catalog_fingerprint"] for job in normalized["machinery"]["jobs"].values(): - var spec: Dictionary = machine_recipe(job["recipe"]) + var spec: Dictionary = machine_recipe(job["recipe"], normalized) if spec.is_empty() or int(job["progress"]) >= int(spec["duration_ticks"]) or int(job["energy_spent"]) != int(job["progress"]) * int(spec["energy_joules"]) / int(spec["duration_ticks"]): return _failure("Invalid machine recipe progress or energy.") if not PaidDoorConstruction.clear_installations(normalized["doors"], normalized): return _failure("Invalid door installation clearance.") var structures: Variant = record.get("structures", {}) diff --git a/src/presentation/production/horizontal_panel_controls.gd b/src/presentation/production/horizontal_panel_controls.gd index 86e788f9..2dfa8d95 100644 --- a/src/presentation/production/horizontal_panel_controls.gd +++ b/src/presentation/production/horizontal_panel_controls.gd @@ -99,7 +99,7 @@ func observe(production: Dictionary) -> void: _pending = "" _record.clear() _panels.clear() - if not ControlContractValidation.is_integer(production.get("schema_version"), 10, 16) or not WorkstationResourceView.valid(production) or int(production["state_revision"]) < _revision or int(production["last_tick"]) < _tick: + if not ControlContractValidation.is_integer(production.get("schema_version"), 10, 17) or not WorkstationResourceView.valid(production) or int(production["state_revision"]) < _revision or int(production["last_tick"]) < _tick: observation_changed.emit({}) _refresh() return diff --git a/src/presentation/production/horizontal_panel_preview.gd b/src/presentation/production/horizontal_panel_preview.gd index 0eae1494..730fb6fe 100644 --- a/src/presentation/production/horizontal_panel_preview.gd +++ b/src/presentation/production/horizontal_panel_preview.gd @@ -12,7 +12,7 @@ var _selected: String = "" func observe(production: Dictionary) -> void: var surfaces: Array = [] - if ControlContractValidation.is_integer(production.get("schema_version"), 10, 16) and HorizontalPanelObservation.valid(production.get("horizontal_panels"), production.get("state_revision")): + if ControlContractValidation.is_integer(production.get("schema_version"), 10, 17) and HorizontalPanelObservation.valid(production.get("horizontal_panels"), production.get("state_revision")): surfaces = production["horizontal_panels"]["surfaces"] var retained: Dictionary = {} for surface in surfaces: diff --git a/src/presentation/production/local_production_panel.gd b/src/presentation/production/local_production_panel.gd index f373006a..545e8722 100644 --- a/src/presentation/production/local_production_panel.gd +++ b/src/presentation/production/local_production_panel.gd @@ -89,8 +89,6 @@ func configure(world: WorldRuntime) -> ServiceResult: _activity = _label() _gather_details = _label() _deposit = _picker() - for id in ["deposit.iron", "deposit.ice", "deposit.regolith", "deposit.iron.outcrop", "deposit.iron.reinforcement"]: - _deposit.add_item(id) _deposit.item_selected.connect(func(_index: int) -> void: refresh()) _extract_button = _button("Extract selected mass into canister (up to 1 kg)", _extract_action) _recipe = _picker() @@ -224,6 +222,7 @@ func refresh() -> void: _guide_label.text = _guide.text() var valid_observation: bool = observed.is_success and typeof(observed.value) == TYPE_DICTIONARY and WorkstationResourceView.valid(observed.value) _record = observed.value if valid_observation else {} + _fill_cargo_picker(_deposit, _record.get("deposits", {}).keys()) _refresh_equipment() _fill_recipes(_record.get("recipes", [])) if not _world is RemoteWorldRuntime: diff --git a/src/presentation/production/timed_canister_controls.gd b/src/presentation/production/timed_canister_controls.gd index b2cc6e74..d5536368 100644 --- a/src/presentation/production/timed_canister_controls.gd +++ b/src/presentation/production/timed_canister_controls.gd @@ -69,7 +69,7 @@ func observe(production: Dictionary) -> void: _epoch = -1 _selected_id = "" _record.clear() - if not ControlContractValidation.is_integer(production.get("schema_version"), 9, 16) or not ControlContractValidation.is_integer(production.get("state_revision"), 0, ProductionSimulationSystem.MAX_REVISION) or int(production["state_revision"]) < _revision or not TimedCanisterConstruction.valid_observation(production.get("timed_canister")): + if not ControlContractValidation.is_integer(production.get("schema_version"), 9, 17) or not ControlContractValidation.is_integer(production.get("state_revision"), 0, ProductionSimulationSystem.MAX_REVISION) or int(production["state_revision"]) < _revision or not TimedCanisterConstruction.valid_observation(production.get("timed_canister")): _refresh() return var observed: Dictionary = production["timed_canister"] diff --git a/tests/godot/commissioned_room_equalization_test.gd b/tests/godot/commissioned_room_equalization_test.gd index 2944d160..62521e6e 100644 --- a/tests/godot/commissioned_room_equalization_test.gd +++ b/tests/godot/commissioned_room_equalization_test.gd @@ -84,7 +84,7 @@ func equalization_checks() -> void: check(room().gas_moles == retained and not pressure_owner().room_valve_open(door_id), "closed reload retains actual gas and position") var controls := PaidDoorControls.new() root.add_child(controls); controls.configure(world, context.principal_id); controls.set_process(false); controls._process(0.2) - check(controls._buttons.size() == 14, "manual bypass actions share collapsed door controls") + check(controls._buttons.size() == 18 and controls._buttons[9].text == "Open bypass to surroundings", "manual bypass actions retain their collapsed door control position") controls._buttons[9].pressed.emit() check(not controls._pending.is_empty(), "actual open-bypass button queues authenticated command") step(); controls._process(0.2) diff --git a/tests/godot/paid_chamber_test.gd b/tests/godot/paid_chamber_test.gd index 74ea815b..754fd729 100644 --- a/tests/godot/paid_chamber_test.gd +++ b/tests/godot/paid_chamber_test.gd @@ -24,6 +24,8 @@ func run() -> void: inventory_action("world", {"operation": "pickup", "item_id": "resource-canister-2", "world_container_id": "world.tray-1", "destination_container_id": "body.human-1", "slot_id": ""}) inventory_action("self", {"operation": "equip", "item_id": "extractor-2", "source_container_id": "storage.backpack-1", "destination_container_id": "", "slot_id": "hand.right"}) inventory_action("self", {"operation": "select_hand", "item_id": "", "source_container_id": "", "destination_container_id": "", "slot_id": "hand.right"}) + move([-3000, 0, -1000]) + catalog_extraction_paths(world.capture_world_snapshot_json().value) for batch in range(39): move([-3000, 0, -1000]) var source: String = "deposit.iron" if batch < 10 else ("deposit.iron.outcrop" if batch < 17 else ("deposit.iron.reinforcement" if batch < 24 else "deposit.iron.chamber")) @@ -74,6 +76,7 @@ func run() -> void: var projection := RemoteProductionProjection.new() check(projection.accept_record(observation).is_success, "real complete v17 projection admits chamber controls") projection.free() + observation_consumers(observation) chamber_negatives(snapshot) detached_chamber_controls(snapshot) legacy_chamber_origins() @@ -384,11 +387,54 @@ func detached_chamber_controls(snapshot: String) -> void: target.free() +func catalog_extraction_paths(snapshot: String) -> void: + var target: WorldRuntime = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + check(target.restore_world_snapshot_json(snapshot).is_success, "isolated extraction catalog control restores actual equipped fixture") + var owner: ProductionSimulationSystem = target.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) + var actor := AuthenticatedCommandContext.for_local_development(context.principal_id, target.world_id, target.region_id) + var before: Dictionary = owner.capture_snapshot().value + var candidate: Dictionary = before.duplicate(true) + check(owner._plan(candidate, "mine", {"expected_revision": before.state_revision, "deposit_id": "deposit.iron.chamber"}).is_success and candidate.job.outputs == {"ore.iron": 1000000}, "catalog4 scheduled mining resolves pinned source material") + var reserved: ServiceResult = owner.apply_local(actor, "isolated-chamber-work", "start_tool_extract", {"expected_revision": before.state_revision, "deposit_id": "deposit.iron.chamber", "carrier_id": "resource-canister-2", "cycles": 1}, int(before.last_tick) + 1) + check(reserved.is_success, "catalog4 timed extraction reserves actual seam: " + reserved.message) + if reserved.is_success: + check(target.start_simulation().is_success, "isolated extraction world starts") + var clock: SimulationRunner = target.get_node("SimulationRunner"); clock.set_physics_process(false) + for tick in range(ToolExtractionWork.CYCLE_TICKS + 1): clock._physics_process(1.0 / 60.0) + check(owner._state.job.is_empty() and owner._state.deposits["deposit.iron.chamber"] == 14000000 and owner._state.cargo["resource-canister-2"]["ore.iron"] == 1000000, "timed completion resolves catalog4 with exact conserved output") + target.free() + + +func observation_consumers(observation: Dictionary) -> void: + var timed := TimedCanisterControls.new(); root.add_child(timed); timed.configure(world) + var controls := HorizontalPanelControls.new(); root.add_child(controls); controls.configure(world) + var preview := HorizontalPanelPreview.new(); root.add_child(preview) + timed.observe(observation); controls.observe(observation); preview.observe(observation) + check(not timed._record.is_empty() and not controls._record.is_empty() and preview._panels.size() == production._state.panels.size(), "fresh-world17 populates actual timed/panel controls and panel preview") + var future: Dictionary = observation.duplicate(true); future.schema_version = 18 + timed.observe(future); controls.observe(future); preview.observe(future) + check(timed._record.is_empty() and controls._record.is_empty() and preview._panels.is_empty() and not WorkstationResourceView.valid(future), "unsupported18 clears controls/preview and rejects resource projection") + timed.free(); controls.free(); preview.free() + check(not production.machine_recipe("extract.iron.chamber").is_empty(), "catalog4 machinery resolves distinct seam recipe") + var exhausted: Dictionary = production._state.duplicate(true) + var no_stock: ServiceResult = production._plan(exhausted, "extract", {"expected_revision": exhausted.state_revision, "deposit_id": "deposit.iron.chamber", "carrier_id": "resource-canister-2", "amount_milligrams": 1}) + check(not no_stock.is_success and exhausted.deposits["deposit.iron.chamber"] == 0, "exhausted chamber source cannot mint extra payload") + + func legacy_chamber_origins() -> void: for mode in [false, true]: var legacy: WorldRuntime = FirstPlayableBootstrap.create_playable_world(mode, false); root.add_child(legacy) var owner: ProductionSimulationSystem = legacy.get_simulation_system(ProductionSimulationSystem.SYSTEM_ID) var before: Dictionary = owner.capture_snapshot().value check(before.catalog_revision == (3 if mode else 2) and not before.deposits.has("deposit.iron.chamber"), "explicit old factory receives no chamber source") + var actor := AuthenticatedCommandContext.for_local_development(&"local-inventory-target", legacy.world_id, legacy.region_id) + for operation in ["extract", "tool_extract", "start_tool_extract", "mine"]: + check(not owner.apply_local(actor, "absent-chamber-" + operation, operation, {"expected_revision": 0, "deposit_id": "deposit.iron.chamber"}, 1).is_success and owner.capture_snapshot().value == before, "legacy catalog rejects absent chamber source without lookup/write:" + operation) + check(owner.machine_recipe("extract.iron.chamber").is_empty(), "legacy machinery never advertises future source") + var panel := ActorPanel.new(); panel.actor = actor; root.add_child(panel); panel.configure(legacy); panel.set_process(false) + var seam_found: bool = false + for option in range(panel._deposit.item_count): seam_found = seam_found or panel._deposit.get_item_text(option) == "deposit.iron.chamber" + check(not seam_found, "legacy actual source selector excludes absent chamber seam") + panel.free() check(legacy.restore_world_snapshot_json(legacy.capture_world_snapshot_json().value).is_success and owner.capture_snapshot().value == before, "legacy reload never grants chamber ore") legacy.free() From 53524cd149c71495d0539b0e17504044b1e2ecc7 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:52:33 -0400 Subject: [PATCH 05/11] Seal both chamber boundaries before fitting paired bypasses --- src/core/production/paid_chamber.gd.uid | 1 + tests/godot/paid_chamber_test.gd | 6 +++++- tests/godot/paid_chamber_test.gd.uid | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/core/production/paid_chamber.gd.uid create mode 100644 tests/godot/paid_chamber_test.gd.uid diff --git a/src/core/production/paid_chamber.gd.uid b/src/core/production/paid_chamber.gd.uid new file mode 100644 index 00000000..50e252c6 --- /dev/null +++ b/src/core/production/paid_chamber.gd.uid @@ -0,0 +1 @@ +uid://cqfjg7jibusq3 diff --git a/tests/godot/paid_chamber_test.gd b/tests/godot/paid_chamber_test.gd index 754fd729..e1cc2f38 100644 --- a/tests/godot/paid_chamber_test.gd +++ b/tests/godot/paid_chamber_test.gd @@ -58,7 +58,11 @@ func run() -> void: if not pair.is_success: finish(); return inner_id = pair.value.inner_door_id; outer_id = pair.value.outer_door_id for id in [inner_id, outer_id]: - door_id = id; door_command("door_seal_install", {"door_id": id}); chamber_ui(8) + door_id = id; door_command("door_seal_install", {"door_id": id}) + if id == inner_id: bypass_reject("door_bypass_install") + for id in [inner_id, outer_id]: + door_id = id; chamber_ui(8) + if failures: finish(); return door_id = inner_id var insufficient: Dictionary = production._state.duplicate(true) insufficient.store.plate = PaidChamber.MASS - 1 diff --git a/tests/godot/paid_chamber_test.gd.uid b/tests/godot/paid_chamber_test.gd.uid new file mode 100644 index 00000000..b5d6788d --- /dev/null +++ b/tests/godot/paid_chamber_test.gd.uid @@ -0,0 +1 @@ +uid://cacq7tf1rnbj5 From 61bb6812573c164228b607003c05073a9de82a02 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:52:57 -0400 Subject: [PATCH 06/11] Explain paired chamber seal and bypass assembly order --- src/core/production/paid_room_bypass.gd | 2 +- src/presentation/production/paid_door_controls.gd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/production/paid_room_bypass.gd b/src/core/production/paid_room_bypass.gd index a83b2b54..5b316157 100644 --- a/src/core/production/paid_room_bypass.gd +++ b/src/core/production/paid_room_bypass.gd @@ -44,7 +44,7 @@ static func plan(state: Dictionary, door_id: String, actuator: ActuatorState, de var other: AccessControlledDeviceState = access.devices.get(StringName(id)) if other == null or pressure.room_area(id) != 0.0 or not PaidChamber.closed_intact(other.actuator(PaidDoorConstruction.actuator_id(id))): candidate_pair = false if operation == "door_bypass_install": - if parts.has(door_id) or (pressure.room_area(door_id) == 0.0 and not candidate_pair) or actuator.position != 0.0 or int(state.store.plate) < MASS: return PaidDoorConstruction.failure("A closed commissioned door and 0.2 kg unreserved plate are required.") + if parts.has(door_id) or (pressure.room_area(door_id) == 0.0 and not candidate_pair) or actuator.position != 0.0 or int(state.store.plate) < MASS: return PaidDoorConstruction.failure("A closed commissioned door or a complete chamber pair with both seals fitted, plus 0.2 kg unreserved plate, is required.") parts[door_id] = {"bypass_id": "room-bypass-%s" % JSON.stringify([String(context.principal_id), request]).sha256_text().substr(0, 32), "blueprint_id": BLUEPRINT, "blueprint_revision": 1, "materials": {"plate": MASS}} state.store.plate -= MASS elif operation == "door_bypass_recover": diff --git a/src/presentation/production/paid_door_controls.gd b/src/presentation/production/paid_door_controls.gd index 9bf7f552..311a56bd 100644 --- a/src/presentation/production/paid_door_controls.gd +++ b/src/presentation/production/paid_door_controls.gd @@ -38,7 +38,7 @@ func configure(world: WorldRuntime, local_principal: StringName = &"local-invent _details.add_child(_doors) _doors.item_selected.connect(func(index: int) -> void: _selected = _doors.get_item_metadata(index)) _status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _status.text = "Requires a paid floor under the full swing. Stand outside the swing within 1.5 m, with a usable crowbar. Seal: 0.2 kg plate. Commission one complete habitat enclosure for finite pressure service; open before decommissioning." + _status.text = "Requires a paid floor under the full swing. Stand outside the swing within 1.5 m of the handle, with a usable crowbar. Seal: 0.2 kg plate. For a two-door chamber, fit both seals first, then both bypasses, then the 6 kg kit on the inner door, then commission. Close the current door and valve before operating the opposite side. Chamber dismantling requires both valves closed and both sides pressure-safe; it may require habitat depressurization. A single habitat compartment uses its separate commission/decommission actions." _details.add_child(_status) for operation in ["door_build", "door_open", "door_close", "door_remove", "door_seal_install", "door_seal_remove", "door_room_commission", "door_room_decommission"] + PaidRoomBypass.OPERATIONS + PaidRoomReinforcement.OPERATIONS + PaidChamber.OPERATIONS: var button := Button.new() From 203b984d6993045975c7710eb65181e32e49e8e7 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:54:54 -0400 Subject: [PATCH 07/11] Compare chamber leak controls in fresh startup runtimes --- tests/godot/paid_chamber_test.gd | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/godot/paid_chamber_test.gd b/tests/godot/paid_chamber_test.gd index e1cc2f38..bc6e6e88 100644 --- a/tests/godot/paid_chamber_test.gd +++ b/tests/godot/paid_chamber_test.gd @@ -347,7 +347,14 @@ func detached_chamber_controls(snapshot: String) -> void: check((gas.volumes[StringName(gas.paid_chamber.volume_id)].gas_moles != moles_before) == leaking, "closed valves do not suppress boundary leak") if leaking: var closed_result: Dictionary = gas.capture_snapshot().value - check(target.restore_world_snapshot_json(JSON.stringify(bad)).is_success, "reset same finite leak control") + # Whole-world restore is startup-only; compare identical fixtures in + # separate runtimes rather than attempting a second startup restore. + target.free() + target = FirstPlayableBootstrap.create_playable_world(); root.add_child(target) + var reset: ServiceResult = target.restore_world_snapshot_json(JSON.stringify(bad)) + check(reset.is_success, "fresh runtime restores same finite leak control") + if not reset.is_success: target.free(); return + gas = target.get_simulation_system(PressureSystem.SYSTEM_ID) gas.room_valves[inner_id] = true gas._step_paid_chamber(1.0 / 60.0) check(gas.capture_snapshot().value.volumes == closed_result.volumes, "open valve plus leak uses one conductance") From f1fbc4a26f8fe50cb875a6d4803e9a1eac4b9fd3 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:58:27 -0400 Subject: [PATCH 08/11] Walk around the chamber door swung leaf in physical regression --- tests/godot/paid_chamber_test.gd | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/godot/paid_chamber_test.gd b/tests/godot/paid_chamber_test.gd index bc6e6e88..ab058658 100644 --- a/tests/godot/paid_chamber_test.gd +++ b/tests/godot/paid_chamber_test.gd @@ -184,8 +184,11 @@ func physical_transit() -> void: actor.position = ProceduralWorldInventoryPreview.DISPLAY_ORIGIN + Vector3(float(original_spawn[0]), maxf(950.0, float(original_spawn[1])), float(original_spawn[2])) / 1000.0 actor.rotation.y = 0.0; actor.read_local_keyboard = false; actor.set_navigation_enabled(true); actor.set_physics_process(false) await walk_to(actor, Vector2(-2.4, -3.5)) + if failures: return await walk_to(actor, Vector2(-1.5, -3.5)) + if failures: return await walk_to(actor, Vector2(-1.5, -2.0)) + if failures: return check(actor.is_on_floor(), "actual spawn-to-inner-approach route remains grounded") var closed_start: Vector3 = actor.position actor.set_motion_intent(Vector2.RIGHT, false) @@ -194,29 +197,43 @@ func physical_transit() -> void: actor.set_motion_intent(Vector2.ZERO, false) # Submit observed reached positions, never reposition the body after spawn. await walk_to(actor, Vector2(-1.5, -3.0)) + if failures: return await walk_to(actor, Vector2(-0.95, -3.0)); bridge_body(actor) + if failures: return door_id = inner_id; chamber_ui(1) transit_remote._receive(publish_door(1)); await physics_frame - await walk_to(actor, Vector2(-1.5, -3.0)) - await walk_to(actor, Vector2(-1.5, -2.0)) + # The opened q1 leaf occupies x[-1.85,-0.95], z[-2.45,-2.4]. + # Walk around its free end, retaining clearance for the whole body. + await walk_to(actor, Vector2(-2.4, -3.0)) + if failures: return + await walk_to(actor, Vector2(-2.4, -2.0)) + if failures: return await walk_to(actor, Vector2(-0.45, -2.0)); bridge_body(actor); chamber_ui(2) + if failures: return check(actor.position.distance_to(closed_start) > 0.5 and actor.is_on_floor(), "actual600mm body crosses inner aperture onto chamber floor") door_id = outer_id; chamber_ui(9); step(600); chamber_ui(10); chamber_ui(1) transit_remote._receive(publish_door(1)); await physics_frame await walk_to(actor, Vector2(1.5, -2.0)); bridge_body(actor); chamber_ui(2) + if failures: return check(actor.is_on_floor(), "actual body fully crosses outer aperture onto extended paid floor") # Return in the opposite direction with the same finite hardware/gas. chamber_ui(1); transit_remote._receive(publish_door(1)); await physics_frame await walk_to(actor, Vector2(-0.45, -2.0)); bridge_body(actor); chamber_ui(2) + if failures: return door_id = inner_id; chamber_ui(9); step(600); chamber_ui(10); chamber_ui(1) transit_remote._receive(publish_door(1)); await physics_frame - await walk_to(actor, Vector2(-1.5, -2.0)) - await walk_to(actor, Vector2(-1.5, -3.0)) + await walk_to(actor, Vector2(-2.4, -2.0)) + if failures: return + await walk_to(actor, Vector2(-2.4, -3.0)) + if failures: return await walk_to(actor, Vector2(-0.95, -3.0)); bridge_body(actor); chamber_ui(2) + if failures: return check(actor.is_on_floor(), "actual body returns through both supported apertures") # Move outside both sweeps for later isolated pressure cycles. await walk_to(actor, Vector2(-1.5, -3.5)); bridge_body(actor) + if failures: return await walk_to(actor, Vector2(0.0, -3.0)); bridge_body(actor) + if failures: return panels.free(); doors.free(); transit_remote.free(); preview.free(); host.free() From 7dec72d265d320a850aef897c1a413f9b1250d6b Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:58:39 -0400 Subject: [PATCH 09/11] Report exact physical collider when chamber walk fails --- tests/godot/paid_chamber_test.gd | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/godot/paid_chamber_test.gd b/tests/godot/paid_chamber_test.gd index ab058658..7ba0b146 100644 --- a/tests/godot/paid_chamber_test.gd +++ b/tests/godot/paid_chamber_test.gd @@ -248,7 +248,12 @@ func walk_to(actor: LocalHabitatCharacter, destination: Vector2) -> void: await physics_frame actor._physics_process(1.0 / 60.0) actor.set_motion_intent(Vector2.ZERO, false) - check(false, "actual body reached continuous walking destination: %s, actual:%s" % [destination, actor.position]) + var contacts: Array[String] = [] + for index in range(actor.get_slide_collision_count()): + var contact: KinematicCollision3D = actor.get_slide_collision(index) + var collider: Object = contact.get_collider() + contacts.append("%s at%s normal%s" % [collider.get_path() if collider is Node else str(collider), contact.get_position(), contact.get_normal()]) + check(false, "actual body reached continuous walking destination: %s, actual:%s, contacts:%s" % [destination, actor.position, contacts]) func bridge_body(actor: LocalHabitatCharacter) -> void: From 5a9de96525bf59045973d93f25d0af139df93451 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 17:59:23 -0400 Subject: [PATCH 10/11] Stop failed chamber walks before commands and clean scene nodes --- tests/godot/paid_chamber_test.gd | 52 ++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/tests/godot/paid_chamber_test.gd b/tests/godot/paid_chamber_test.gd index 7ba0b146..434e55fd 100644 --- a/tests/godot/paid_chamber_test.gd +++ b/tests/godot/paid_chamber_test.gd @@ -179,16 +179,18 @@ func physical_transit() -> void: check(preview._paid_worksite_legacy_collider.disabled and not preview._paid_worksite_legacy_wall.visible, "production focus removes only exact obsolete worksite obstruction") var doors := PaidDoorContactScene.new(); root.add_child(doors); doors.bind(transit_remote); doors.set_physics_process(false) var panels := PaidPanelContactScene.new(); root.add_child(panels); panels.bind(transit_remote); panels.set_physics_process(false) + var cleanup := func() -> void: + panels.free(); doors.free(); transit_remote.free(); preview.free(); host.free() transit_remote._receive(publish_door(1)) var actor: LocalHabitatCharacter = preview.character actor.position = ProceduralWorldInventoryPreview.DISPLAY_ORIGIN + Vector3(float(original_spawn[0]), maxf(950.0, float(original_spawn[1])), float(original_spawn[2])) / 1000.0 actor.rotation.y = 0.0; actor.read_local_keyboard = false; actor.set_navigation_enabled(true); actor.set_physics_process(false) await walk_to(actor, Vector2(-2.4, -3.5)) - if failures: return + if failures: cleanup.call(); return await walk_to(actor, Vector2(-1.5, -3.5)) - if failures: return + if failures: cleanup.call(); return await walk_to(actor, Vector2(-1.5, -2.0)) - if failures: return + if failures: cleanup.call(); return check(actor.is_on_floor(), "actual spawn-to-inner-approach route remains grounded") var closed_start: Vector3 = actor.position actor.set_motion_intent(Vector2.RIGHT, false) @@ -197,44 +199,48 @@ func physical_transit() -> void: actor.set_motion_intent(Vector2.ZERO, false) # Submit observed reached positions, never reposition the body after spawn. await walk_to(actor, Vector2(-1.5, -3.0)) - if failures: return - await walk_to(actor, Vector2(-0.95, -3.0)); bridge_body(actor) - if failures: return + if failures: cleanup.call(); return + await walk_to(actor, Vector2(-0.95, -3.0)) + if failures: cleanup.call(); return door_id = inner_id; chamber_ui(1) transit_remote._receive(publish_door(1)); await physics_frame # The opened q1 leaf occupies x[-1.85,-0.95], z[-2.45,-2.4]. # Walk around its free end, retaining clearance for the whole body. await walk_to(actor, Vector2(-2.4, -3.0)) - if failures: return + if failures: cleanup.call(); return await walk_to(actor, Vector2(-2.4, -2.0)) - if failures: return - await walk_to(actor, Vector2(-0.45, -2.0)); bridge_body(actor); chamber_ui(2) - if failures: return + if failures: cleanup.call(); return + await walk_to(actor, Vector2(-0.45, -2.0)) + if failures: cleanup.call(); return + chamber_ui(2) check(actor.position.distance_to(closed_start) > 0.5 and actor.is_on_floor(), "actual600mm body crosses inner aperture onto chamber floor") door_id = outer_id; chamber_ui(9); step(600); chamber_ui(10); chamber_ui(1) transit_remote._receive(publish_door(1)); await physics_frame - await walk_to(actor, Vector2(1.5, -2.0)); bridge_body(actor); chamber_ui(2) - if failures: return + await walk_to(actor, Vector2(1.5, -2.0)) + if failures: cleanup.call(); return + chamber_ui(2) check(actor.is_on_floor(), "actual body fully crosses outer aperture onto extended paid floor") # Return in the opposite direction with the same finite hardware/gas. chamber_ui(1); transit_remote._receive(publish_door(1)); await physics_frame - await walk_to(actor, Vector2(-0.45, -2.0)); bridge_body(actor); chamber_ui(2) - if failures: return + await walk_to(actor, Vector2(-0.45, -2.0)) + if failures: cleanup.call(); return + chamber_ui(2) door_id = inner_id; chamber_ui(9); step(600); chamber_ui(10); chamber_ui(1) transit_remote._receive(publish_door(1)); await physics_frame await walk_to(actor, Vector2(-2.4, -2.0)) - if failures: return + if failures: cleanup.call(); return await walk_to(actor, Vector2(-2.4, -3.0)) - if failures: return - await walk_to(actor, Vector2(-0.95, -3.0)); bridge_body(actor); chamber_ui(2) - if failures: return + if failures: cleanup.call(); return + await walk_to(actor, Vector2(-0.95, -3.0)) + if failures: cleanup.call(); return + chamber_ui(2) check(actor.is_on_floor(), "actual body returns through both supported apertures") # Move outside both sweeps for later isolated pressure cycles. - await walk_to(actor, Vector2(-1.5, -3.5)); bridge_body(actor) - if failures: return - await walk_to(actor, Vector2(0.0, -3.0)); bridge_body(actor) - if failures: return - panels.free(); doors.free(); transit_remote.free(); preview.free(); host.free() + await walk_to(actor, Vector2(-1.5, -3.5)) + if failures: cleanup.call(); return + await walk_to(actor, Vector2(0.0, -3.0)) + if failures: cleanup.call(); return + cleanup.call() func walk_to(actor: LocalHabitatCharacter, destination: Vector2) -> void: From 7f0aaa2590ecdc2180133a07629db1f7de17387f Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 23 Oct 2026 18:00:59 -0400 Subject: [PATCH 11/11] Keep unsupported observation controls above newly supported version17 --- tests/godot/horizontal_panel_controls_test.gd | 2 +- tests/godot/timed_canister_controls_test.gd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/godot/horizontal_panel_controls_test.gd b/tests/godot/horizontal_panel_controls_test.gd index 1d9c99b7..20b2b540 100644 --- a/tests/godot/horizontal_panel_controls_test.gd +++ b/tests/godot/horizontal_panel_controls_test.gd @@ -133,7 +133,7 @@ func run() -> void: controls.observe(frame()["production"]) controls.observe(JSON.parse_string(JSON.stringify(frame()["production"]))) check(not controls._record.is_empty() and not preview._panels.is_empty(), "integral JSON version preserves controls and paid panel preview") - for version in [true, 10.5, 17, "13"]: + for version in [true, 10.5, 18, "13"]: var bad_version: Dictionary = frame()["production"].duplicate(true) bad_version.schema_version = version controls.observe(bad_version) diff --git a/tests/godot/timed_canister_controls_test.gd b/tests/godot/timed_canister_controls_test.gd index 48518e6a..7ee6fe14 100644 --- a/tests/godot/timed_canister_controls_test.gd +++ b/tests/godot/timed_canister_controls_test.gd @@ -238,7 +238,7 @@ func projection_checks(current: Dictionary, pre_completion: Dictionary) -> void: remote_controls.configure(remote) remote_controls.observe(remote.production_observation(context).value) check(not remote_controls._record.is_empty(), "remote UI reads real completed host projection") - for version in [true, 10.5, 17, "13"]: + for version in [true, 10.5, 18, "13"]: var bad_version: Dictionary = remote.production_observation(context).value.duplicate(true) bad_version.schema_version = version remote_controls.observe(bad_version)