Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21b5d3a31a | ||
|
|
0b7727a66c | ||
|
|
55c29f1192 | ||
|
|
baa4f2d563 | ||
|
|
59a1385536 | ||
|
|
d9e4e9bd2f | ||
|
|
f6176d3f19 | ||
|
|
4c8a18aa75 | ||
|
|
03100beabe |
@@ -3702,6 +3702,17 @@
|
||||
"scope": "normal placement; native and OS pending"
|
||||
}
|
||||
}
|
||||
,{
|
||||
"test": "tests/ui/test_p23_life_observation.gd",
|
||||
"template": "P23 life observation: <checks> checks, <failures> failures",
|
||||
"required": true,
|
||||
"fixed_check_count_used": false,
|
||||
"source_sha256_at_schema_audit": "cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1",
|
||||
"authority": {
|
||||
"path": "docs/spec/acceptance/P23-03.json",
|
||||
"scope": "component and read-only observation only; native unexecuted; whole unit blocked"
|
||||
}
|
||||
}
|
||||
],
|
||||
"unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting",
|
||||
"roster_count_is_not_hardcoded": true,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# P23-03 Phase 0 — API and state map
|
||||
|
||||
Base: `860136f7099f5e3aee99da5798a627a77f2d8eea`. Static inspection only; no engine execution.
|
||||
|
||||
## Exact source references
|
||||
|
||||
Line numbers below refer to the base; SHA256 covers the complete file.
|
||||
|
||||
| Source | Lines / API | SHA256 |
|
||||
| --- | --- | --- |
|
||||
| autoload/simulation_runtime.gd | 203 initialize(clock,state,seed); 225 seed assignment; 466 submit_command; 508 get_rng_stream; 511 create_snapshot; 1483 enable_production_operations; 1780 default demand configuration; 2831 arrival boundary; 2949 apply arrivals; 4137 queue_manual_checkpoint; 4141 save_to_path; 4169 load_from_path; 5066 seed streams; 6103 spawn_guest executor; 7629 build_facility executor; 10859 staff management report; 12464 supply management report | 4696df4a77feb465a5a3ba8b806569f7bcbe1d03c88289acfb78a3928f307d67 |
|
||||
| autoload/game_state.gd | 20 reset | cf0e884910220e54c56b5875983e53de439af9d893be1e1b65c2121d2a30722f |
|
||||
| scripts/core/simulation_kernel.gd | 91 reset; 104 advance_minutes; 122 advance_one_minute; 175 set_paused | 341c0c878c4894adbac628d0db6362ff69fc3347bdd9e61e50936e15dc2d1edb |
|
||||
| scripts/facility/facility_service.gd | 109 get_instances; 656 create_snapshot | ceb4a3e9224587423f273cdb2b56e6843ab176ac1fc4fe79dd498b5ed5c33b77 |
|
||||
| scripts/guest/guest_service.gd | 170 get_guests; 982 create_snapshot | 56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e |
|
||||
| scripts/staff/staff_service.gd | 166 get_staff_members; 682 get_tasks; 1064 create_snapshot | 355ad5c3c9f85dc4d4ffc3bc4293e36f81078c403d1662512a735b2c8d815fb3 |
|
||||
| scripts/transport/elevator_service.gd | 345 get_report; 362 create_snapshot | cea09dd9204551c26b31b30cd3aa847cc77e3b75fdac31662b9694a95fb3580c |
|
||||
| scripts/world/path_service.gd | 545 get_cell_traffic | 447135729a1106639a6eee6754af3195c8a8b458ca26980ae91fa39d80dcfbd4 |
|
||||
| scripts/supply/supply_logistics_bridge.gd | 122 get_report | b9e394af788595d2744b71f1907c8e7c3d762d38ff49992b1ee74e63b9639e80 |
|
||||
| scripts/facility/facility_instance.gd | 8 OperationState; 10 State; 30 state and subsequent live fields | 0c6aa493f3299611e05dd26aa429dda8cf7b5c479e4d09632036fb862f79a324 |
|
||||
| tests/transport/test_runtime_transport_comparison.gd | 91 existing fixed-demand setup; 109 real paid build command | ba577397fa8db5a6cf92cfd398dd7a5ece49e6b59856f14772b67b51ddc0bde9 |
|
||||
|
||||
## Deterministic demand capability
|
||||
|
||||
An existing deterministic input path is available without Runtime edits: fresh clock/state, `initialize(clock,state,seed)`, then the same scheduled `submit_command(&"spawn_guest", payload, ..., execute_at, idempotency_key)` sequence. The executor validates registered definitions and real walkable cells, creates wallets and GuestRuntime actors, and verifies Path actor projection and movement credit. Explicit IDs must avoid the reserved `guest-auto-` namespace. Inputs and processed results must both be recorded; a seed/hash alone does not prove identical arrivals.
|
||||
|
||||
The existing transport comparison exercises public spawning, real paid construction and Runtime-owned movement. It is fixed-demand experimentation, not proof of natural autonomous demand or all six observation categories. Its numeric assertions are not adopted as new acceptance thresholds. Automatic production arrivals are a separate path (`enable_production_operations`): population, weather and terminal-state gates mean the same seed alone cannot guarantee identical accepted arrivals. Layouts must use identical production settings and compare actual arrival inputs/results.
|
||||
|
||||
`build_facility` resolves the registered definition, runs `_build_facility_precheck`, then calls `construction_service.start_construction` with `definition.build_minutes`. Construction cost, time and capacities must come from definitions. B may add only the predeclared existing corridor/EV through this command.
|
||||
|
||||
## Clock and observation
|
||||
|
||||
`reset` defines initial time. Paused manual `advance_minutes` is explicitly supported; abort/pause boundaries must be checked. Commands run before minute increment, then time boundaries, mana, construction, logistics, facility completion, vitals, incidents, queues, AI and publication.
|
||||
|
||||
Facility/guest/staff getters expose typed objects; presentation must copy values without mutating them. Elevator reports contain shafts/trips. Path traffic provides actual density/capacity/block fields. Logistics reports contain requests/deliveries, source/destination IDs, carrier, requirements, weight, state/reason and task ID. They do not contain warehouse stock: requested quantities must never be labelled stock.
|
||||
|
||||
## Formal save/load and evidence
|
||||
|
||||
Use `queue_manual_checkpoint` or `save_to_path`, then `load_from_path` under a distinct real PID. The load API pauses the clock and invokes SaveService package validation before restore. `restore_snapshot` alone is not formal resume. Preserve all package members, source/spec/engine/settings pins, whole typed sparse snapshots and actual CUA/process/cleanup evidence.
|
||||
|
||||
Original references read: BASELINE.txt lines 10,13,16,25,37,43,69,83,106,216; IMPLEMENTATION_PLAN.md line 1800; immutable P23-03 spec; recorded direction 83e836e. All seven conditions remain mandatory, with no partial credit. Root native/OS/resume/full-regression evidence is missing.
|
||||
@@ -0,0 +1,253 @@
|
||||
# P23-03 offline repair — evidence gap
|
||||
|
||||
Status: blocked, not static-complete. No acceptance credit. The failed 0b7727a
|
||||
OS run remains failure history. No receipts, mirror files, product, values,
|
||||
styles or test gates were changed. No git writes or engine/OS/collector/full
|
||||
runs were performed.
|
||||
|
||||
## Corrected direction
|
||||
|
||||
The three previous pixel assumptions are superseded:
|
||||
|
||||
1. `viewport_to_screen` maps to content coordinates, not screenshot pixels.
|
||||
Actual request-0002 and request-0004 CUA receipt calibration records window
|
||||
size 1440x810, screenshot 1440x842, titlebar 32, backing scale 1 and stretch 1.
|
||||
The image lookup must apply the calibrated content origin and independently
|
||||
check agreement with actual CUA click offsets; the number 32 must not become
|
||||
a checker constant. Existing affine composition checks remain required.
|
||||
2. Source RGB cannot be required unchanged after JPEG conversion, NO_STAFF
|
||||
dimming, ghost outlines and antialiasing. Palette identity is source
|
||||
provenance, not a screenshot color oracle. Pixel proof needs measured
|
||||
within-image noise and independent expected-class/state/shift alternatives.
|
||||
3. Every report row must be accounted for, but not every row is visible. Use
|
||||
report/audit fields for visible, clipped/clamped, outside with reason,
|
||||
ghost/inactive and facility-state dimming. Require eligible visible coverage
|
||||
of all six categories on both FRONT and BACK. Never silently skip a row.
|
||||
|
||||
The spec requires draw-consumed rows, pixel rectangles and provenance to
|
||||
correspond; it does not require unchanged source RGB. The function docstring
|
||||
now explicitly warns that the implementation still uses the superseded checks.
|
||||
|
||||
## Verified material and blocking dependency
|
||||
|
||||
Mirror:
|
||||
`/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07`
|
||||
|
||||
All 94 files named by MIRROR_INDEX.json were read and their SHA256 verified;
|
||||
zero missing files or mismatches. The only indexed conversion record is
|
||||
`os-pair/A/writer-request-0002/root-early-conversion.json`, with stdout/stderr.
|
||||
The only decoded PNG is the corresponding `root-early-decoded.png`, SHA256
|
||||
`206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a`.
|
||||
|
||||
The other three required stable screenshots are JPEG bytes despite .png names:
|
||||
|
||||
- `os-pair/A/writer-request-0002/screen-after.png`
|
||||
- `os-pair/A/writer-request-0004/screen-before.png`
|
||||
- `os-pair/A/writer-request-0004/screen-after.png`
|
||||
|
||||
Their magic starts `ffd8ffe000104a46`. No frozen decoded PNG/conversion records
|
||||
for these files are in the mirror. Searching the original source root named
|
||||
by MIRROR_INDEX for `*decoded*.png` and `*conversion*.json` also returned none.
|
||||
The existing conversion verifier requires a separate PNG, original raw hash,
|
||||
CUA call attribution and pinned `/usr/bin/sips` conversion record. Creating
|
||||
replacement conversion assertions, rewriting receipt paths, or silently
|
||||
changing that converter authority would not preserve this proof gate.
|
||||
|
||||
Minimal next input: Root supplies separate decoded PNGs and genuine conversion
|
||||
records/logs for those three existing raw images, adds their hashes to a new
|
||||
mirror inventory while preserving the original inventory and receipts. This
|
||||
does not require another OS capture. Alternatively the commander must explicitly
|
||||
specify an offline Python JPEG decoder authority and its provenance contract.
|
||||
Worker must not execute sips under the Python-only direction.
|
||||
|
||||
Until then the requested full frozen before/after comparison, BACK cropped
|
||||
pixel fixtures, calibrated noise and per-row pixel results cannot be submitted
|
||||
as complete. Geometry/report preparation alone cannot meet all requested tests.
|
||||
The behavioral repair and new fixtures/tests have not been implemented.
|
||||
|
||||
## Local commands and outputs
|
||||
|
||||
These read-only Python checks used the mirror path above as `p`:
|
||||
|
||||
```python
|
||||
import json, pathlib, hashlib
|
||||
p = pathlib.Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07')
|
||||
j = json.loads((p / 'MIRROR_INDEX.json').read_text())
|
||||
bad = [r['path'] for r in j['files'] if not (p/r['path']).is_file()
|
||||
or hashlib.sha256((p/r['path']).read_bytes()).hexdigest() != r['sha256']]
|
||||
print('indexed', len(j['files']), 'mismatches', bad)
|
||||
print('conversion records', [r['path'] for r in j['files'] if 'conversion' in r['path']])
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
indexed 94 mismatches []
|
||||
conversion records ['os-pair/A/writer-request-0002/root-early-conversion.json', 'os-pair/A/writer-request-0002/root-early-conversion.json.stderr', 'os-pair/A/writer-request-0002/root-early-conversion.json.stdout']
|
||||
```
|
||||
|
||||
Calling the existing `screenshot_pixels(p/name)` for each of the three paths
|
||||
above (without manufacturing a conversion argument) produced:
|
||||
```
|
||||
writer-request-0002/screen-after.png: JPEG screenshot requires conversion record
|
||||
writer-request-0004/screen-before.png: JPEG screenshot requires conversion record
|
||||
writer-request-0004/screen-after.png: JPEG screenshot requires conversion record
|
||||
```
|
||||
|
||||
Command: `python3 tools/test_p23_life_observation_evidence.py`
|
||||
```
|
||||
................................
|
||||
----------------------------------------------------------------------
|
||||
Ran 32 tests in 0.058s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
These are existing unit tests, not the requested new repair tests, and do not
|
||||
prove the repair or acceptance. Root native/OS/full review remains pending.
|
||||
Seven conditions, six categories, 30/88, no partial credit, timeout 900 and
|
||||
cap null remain unchanged.
|
||||
|
||||
## Follow-on: 106-entry mirror (2026-10-15)
|
||||
|
||||
The missing-conversion dependency above is **resolved**, not a current blocker.
|
||||
The prior text is retained as failure history. The worker read all 106 indexed
|
||||
files and verified every byte count and SHA256, plus the preserved initial94
|
||||
inventory SHA and unchanged entries. No mirror or original receipt was edited.
|
||||
|
||||
`OFFLINE_MIRROR_106_DIAGNOSTICS.json` contains the explicit original absolute
|
||||
path -> mirrored absolute path -> equal indexed SHA256 mapping for all106 files,
|
||||
including the eight named PNG/conversion files. Each conversion was checked
|
||||
against its corresponding actual CUA screenshot call ID/raw hash, pinned sips
|
||||
binary SHA, original argv, output PNG magic/dimensions/hash and stdout/stderr
|
||||
hashes. Original raw/png path strings and argv remain unchanged. A read-only
|
||||
resolver locates indexed mirrored bytes; it does not rewrite conversion records.
|
||||
Direct reads of the source artifact directory were denied by the host
|
||||
(`PermissionError: Operation not permitted`); equality to the frozen inventory
|
||||
and conversion-record hashes was verified, not a fresh direct source-directory
|
||||
comparison. This limitation is not represented as a missing PNG conversion.
|
||||
|
||||
New `--offline-mirror` preparation diagnostics check content/image dimensions,
|
||||
backing scale, content origin derived from recorded titlebar, affine composition,
|
||||
and agreement with the separately recorded click events. They independently
|
||||
derive existing audit row identities/local rectangles from report positions and
|
||||
source palette provenance. Every existing audit row has visible/clipped/outside
|
||||
accounting and a calibrated screenshot rectangle. Room enum/operation names are
|
||||
read from the actual FacilityInstance source. Dimming is derived from operation,
|
||||
not merely the NO_STAFF state name. These are preparation diagnostics, not a
|
||||
replacement acceptance pixel gate. In particular, audit font_width remains an
|
||||
engine assertion, and receipt click events have not been verified against the
|
||||
full helper-definition/tool trace. No source RGB tolerance was invented.
|
||||
|
||||
Actual frozen-frame results:
|
||||
|
||||
- FRONT request0002: 9 audit rows (3 room,1 queue,2 guest,3 business), all visible.
|
||||
EV/staff/goods coverage pending. Two rooms are INACTIVE and one OPERATING.
|
||||
One actual draw-consumed traffic overlay at cell[-6,2,0], local rect
|
||||
[261,294,60,92], has no corresponding rectangle-audit row.
|
||||
- BACK request0004: 4 audit rows (2 room,2 business), all visible and both rooms
|
||||
INACTIVE. Queue/EV/staff/goods coverage pending; no eligible traffic overlay.
|
||||
- Before/after temporal differences within all13 calibrated audit rectangles
|
||||
are zero. This establishes stability only, not spatial JPEG error or correct
|
||||
drawing class/state. It supplies no pixel acceptance credit.
|
||||
- The existing independent typed-stream checker recomputed all14 frozen stream
|
||||
records successfully; the CLI still returned blocked/pending(exit2), as required.
|
||||
|
||||
Current repair status: **incomplete / blocked**, not genuinely submitted.
|
||||
The legacy `compare_rectangles` and its normal-OS call site have deliberately
|
||||
not been relabelled as repaired. Remaining worker work includes an independent
|
||||
expected-class/state/shift pixel discriminator with measured within-image noise,
|
||||
state negatives, full ghost/inactive/traffic draw accounting, independent badge
|
||||
width provenance and integration into the strict OS checker. The added
|
||||
preparation path does not solve these items and must not be used for acceptance.
|
||||
Missing frozen provenance includes the FRONT traffic-overlay rectangle audit,
|
||||
opposite-layer ghost draw audit, and independent font measurements for badge
|
||||
widths. Existing reports/source can support further preparation; these missing
|
||||
records do not establish that a fresh OS capture is already necessary.
|
||||
|
||||
Local validation: Python AST syntax checks; 42 checker unit tests (existing32
|
||||
plus10 preparation tests); actual mirrored checker commands above. Tests cover
|
||||
wrong titlebar/click shifts, PID, nonfinite scales, clipped/outside rectangles,
|
||||
measured temporal noise, report rectangle/identity/coverage/palette tampering.
|
||||
They do not include the still-required pixel-state discriminator negatives.
|
||||
No Godot, native, sips execution, collector, OS run, full regression, git write,
|
||||
branch/integration, Runner action or agent delegation occurred. Root native,
|
||||
complete A/B FRONT/BACK pan/zoom/distinct-PID OS matrix, mandatory full regression
|
||||
and independent Opus review remain pending. Historical baselines remain32 tests,
|
||||
17+17 semantic,319 native,7153/2585 and7/6/30/88; the new local42 count is not a
|
||||
replacement acceptance baseline. Timeout900 and cap null are unchanged.
|
||||
|
||||
## Owner proof-method repair (2026-10-15)
|
||||
|
||||
The owner's recorded decision supersedes the proposed automatic class/state/
|
||||
style discriminator, shift/swap alternative scoring, measured-noise gate and
|
||||
independent badge-width measurement gate. They are no longer required work.
|
||||
Palette RGB is provenance only. Font width remains the recorded engine value;
|
||||
no independent font measurement or numeric image tolerance was introduced.
|
||||
All earlier entries above are retained as history, not current requirements.
|
||||
|
||||
Implemented within the narrowed Python edit scope:
|
||||
|
||||
- Shared `frame_rows` now handles both offline and normal OS screenshot frames,
|
||||
uses receipt calibration checked against actual click events, checks affine
|
||||
composition, independently derives row identity/local geometry/text/palette,
|
||||
validates room state/operation and actor enum names, and retains every audit
|
||||
row with visibility, clipping reason, calibrated image rectangle and failures.
|
||||
- The legacy `compare_rectangles` and its tests are removed and replaced by
|
||||
grounded negatives. Fully outside eligible image crops and exactly uniform
|
||||
crops fail; source RGB is never compared to screenshot RGB.
|
||||
- `drawing_rectangle_rows` now expects every draw-consumed traffic overlay.
|
||||
Missing/extra rows fail the mechanical frame, without synthesizing audit rows.
|
||||
- Pure-Python PNG crops and review manifests are written for each frozen frame.
|
||||
Manifests bind decoded whole-image/raw/conversion/request/receipt hashes,
|
||||
per-row source hash, recorded font width, state, operation, layer and geometry.
|
||||
A separate SHA file hashes the exact manifest bytes. Normal OS frames use the
|
||||
same path and emit per-image review materials. Pixel categories receive no
|
||||
credit without an independent direct-image review verdict bound to that SHA,
|
||||
covering every eligible row with category/state/position verdicts. The worker
|
||||
created no actual review verdict; synthetic verdict files exist only inside
|
||||
temporary unit-test fixtures.
|
||||
|
||||
Current scope blocker, found by reading the actual dependencies:
|
||||
`tests/fixtures/p23-03/passive_observation.gd:150` owns `draw_rectangles`, not
|
||||
`world_preview.gd`. Its explicit room/queue/cabin/actor/badge loops at lines
|
||||
152-171 do not consume traffic audit rows from the view or report. The transform
|
||||
and palette audit path at lines172-182 only processes that locally built array.
|
||||
`world_preview.gd:762` draws the traffic overlay, but has no existing rectangle
|
||||
audit emitter or callback. Changing only world_preview cannot connect traffic
|
||||
rows to the actual request's `rectangles` field without altering the fixture.
|
||||
No inert emitter, mislabelled badge, or checker-generated observed audit was
|
||||
added to pretend this connection exists. World source remains unchanged.
|
||||
|
||||
Minimal separate scope proposal: permit one read-only traffic-row loop in
|
||||
`passive_observation.gd::draw_rectangles`, before its existing transform/palette
|
||||
loop, consuming report traffic rows with density>0 or temporary_block. Use
|
||||
category `traffic`, comma-separated cell coordinates as id, the report rectangle
|
||||
and `scenes/main/world_preview.gd` as style source. Alternatively explicitly
|
||||
authorize wiring a view-owned traffic audit emitter into that same fixture.
|
||||
The broad contract lists this fixture, but the latest finish recipe explicitly
|
||||
narrows edits to checker/tests and world only; the worker did not override it.
|
||||
Step4 is therefore incomplete and this repair is **blocked**, not submitted.
|
||||
|
||||
Local validation: both Python files parse with `ast.parse`; 47 local checker
|
||||
tests pass. New tests exercise blank crops, missing/extra/traffic rows, enum/name
|
||||
errors, affine/click calibration shifts, image-outside crops, exact crop hashes,
|
||||
wrong-manifest/missing/extra/rejected review rows and no credit without review.
|
||||
The r07 offline CLI verified all106 indexed entries and exited2 as blocked:
|
||||
|
||||
- FRONT request0002: expected10 rows, observed9; mechanical failure
|
||||
`offline rectangle coverage`. The traffic overlay at [-6,2,0] remains a genuine
|
||||
unaudited overlay. EV/staff/goods coverage pending; zero pixel credit.
|
||||
- BACK request0004: expected4/observed4; mechanical checks pass.
|
||||
Queue/EV/staff/goods coverage pending; zero pixel credit.
|
||||
|
||||
Current review artifacts are in `offline-r07-review/`; complete diagnostics are
|
||||
in `OFFLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json`. The prior
|
||||
`OFFLINE_MIRROR_106_DIAGNOSTICS.json` is preserved unchanged.
|
||||
|
||||
Still pending: Root frozen native checks, fresh A/B FRONT/BACK ordinary pan/zoom
|
||||
OS observation and formal distinct-PID resume, the exact full177 regression,
|
||||
and independent Opus direct-image/all-condition review. No Godot, Runner,
|
||||
native/OS, full regression, git write, integration or agent execution occurred.
|
||||
All7 conditions, all6 categories on both layers/layouts, source recomputation,
|
||||
raw/request/CUA/conversion provenance, calibrated geometry, 30/88 without partial
|
||||
credit, timeout900 and cap null remain unchanged. No acceptance PASS is claimed.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Offline checker repair on 55c29f1
|
||||
|
||||
Source HEAD: 55c29f119234dd6f993c604c4edc6f377fe3951c. Registration base remains
|
||||
860136f7099f5e3aee99da5798a627a77f2d8eea. No git writes or engine execution.
|
||||
|
||||
Read access: MIRROR_INDEX.json and all 2,622 named files were successfully read
|
||||
under `/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-evidence-55c29f1-r06`.
|
||||
No mirror bytes were changed. The initial shell heredoc could not create its
|
||||
temporary file; the read check was executed using Python `-c` instead.
|
||||
|
||||
## First defects and production cause
|
||||
|
||||
Both layouts first failed at frame-030, checker line 328, with KeyError:
|
||||
`shaft_id`. State objects:
|
||||
|
||||
- A: `layout-A/objects/ac3ed64a6e6162918e4d44c45b2f78d09e50d473b607f3469350a06074a7efb3.json`
|
||||
- B: `layout-B/objects/97ea43afe3d1b21602ebe2a602e7b7d796ff05f03983cd4d1b3edc9157e39a08.json`
|
||||
|
||||
The exception came from `shaft['shaft_id']`, not `t['shaft_id']`. Every trip in
|
||||
these failing frames has shaft_id. Saved shaft rows have it only in config.
|
||||
There is no evidence of shaft-less QUEUED trips to support accepting that shape.
|
||||
|
||||
`autoload/simulation_runtime.gd:541` writes elevators.create_snapshot().
|
||||
`scripts/transport/elevator_service.gd:362-367` copies the saved cars without
|
||||
flattening config; get_report at 345-359 merges config into each report row before
|
||||
counting queued trips. request at 169-171 assigns shaft_id on creation, and the
|
||||
restore validator at 412-454 requires the exact trip keys and shaft/connector
|
||||
agreement. The connector spelling is defined at 617-618; trips are ordered at
|
||||
707-710. `scenes/main/world_preview.gd:973-975` consumes get_report and sorts
|
||||
trips by enqueue time/actor. The checker now counts using the merged row identity.
|
||||
|
||||
## Reviewable changes
|
||||
|
||||
- `tools/check_p23_life_observation_evidence.py`: replace
|
||||
`t['shaft_id'] == shaft['shaft_id']` with `t['shaft_id'] == row['shaft_id']`;
|
||||
validate exact trip keys, companion identity, shaft/stops/connector attribution
|
||||
and QUEUED/BOARDED state before recomputing. Whole report equality remains strict.
|
||||
- `tests/fixtures/p23-03/queued_elevator.json`: retain one actual frame-030
|
||||
queued trip and its shaft, without a cabin seat. Distilled unit data only.
|
||||
- `tools/test_p23_life_observation_evidence.py`: queued shape positive and
|
||||
shaft attribution, waiting_count, missing/extra key negatives; update two
|
||||
pre-existing abbreviated synthetic fixtures to the actual shaft/trip shape.
|
||||
- This repair report. The complete implementation diff is available through
|
||||
`git diff -- tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`;
|
||||
the new fixture and this file are untracked submission files.
|
||||
|
||||
## Local results
|
||||
|
||||
`inspect_stream` on each exact absolute mirror directory:
|
||||
|
||||
```
|
||||
layout-A inspect_stream PASS 17 frames
|
||||
layout-B inspect_stream PASS 17 frames
|
||||
```
|
||||
|
||||
For both, labels are frame-000, frame-030, frame-060, frame-090, frame-120,
|
||||
frame-150, frame-180, frame-210, frame-240, frame-270, frame-300, frame-330,
|
||||
frame-360, frame-390, frame-420, frame-450, frame-480. Each returns fields:
|
||||
guests, staff, tasks, elevators, room_service_fields, queues, traffic, goods,
|
||||
cabin_geometry. Every frame also passes the existing two-motion-history-frame
|
||||
check, unchanged. No later schema/semantic mismatch was observed.
|
||||
|
||||
Python syntax AST checks pass. Local checker tests: 32 tests, OK.
|
||||
The initial test run exposed two abbreviated synthetic fixtures; subsequent
|
||||
queue expectations were corrected for actual lift landing exclusion. Tests
|
||||
were preserved. git diff --check passes; changed files are within allowed_paths.
|
||||
completion_marker_contracts retains its one required P23 strict template entry;
|
||||
no marker changes. Seven conditions, six categories and 30/88 remain unchanged.
|
||||
|
||||
Root native results (319 tests, A/B PASS), matched metric values (48 metrics,
|
||||
7153/2585), production data and services were not modified. Root OS evidence
|
||||
remains pending as expected at worker submission; this repair grants no acceptance.
|
||||
@@ -0,0 +1,296 @@
|
||||
# P23-03 worker status — static-complete submission
|
||||
|
||||
Contract base: `860136f7099f5e3aee99da5798a627a77f2d8eea`.
|
||||
This finalization preserves every pre-existing modified and untracked file.
|
||||
Static-complete is supported by the wiring inspection and local checks below.
|
||||
No acceptance claim is made. No Godot, native, OS collector, Runner, full
|
||||
regression, git write, branch change, or other worker was used.
|
||||
|
||||
## Current wiring confirmed by git diff and rg only
|
||||
|
||||
Line numbers refer to the current candidate.
|
||||
|
||||
- Checker `tools/check_p23_life_observation_evidence.py`: `check` at 1189
|
||||
reaches `inspect_native_phases` (1162) at 1215 and `inspect_stream` (950)
|
||||
at 1220/1226. The stream inspection invokes `compare_rest` (531) at 968,
|
||||
`compare_motion` (738) at 974, and `compare_actor_geometry` (800) at 975,
|
||||
alongside actor, room, queue, traffic, goods, badge and cabin checks.
|
||||
- `check` reaches `inspect_os_receipt` (1038), `inspect_os_requests` (1092),
|
||||
and `inspect_resume` (1070) at 1249, 1250 and 1257.
|
||||
Request inspection calls `screenshot_pixels` (184) at 1143, which invokes
|
||||
`inspect_conversion` (158) at 191 for JPEG evidence; rectangle/pixel
|
||||
checking follows at 1154.
|
||||
- `check` builds executed check sets at 1270–1272, then calls `BLOCKERS`
|
||||
at 1273, reaching `condition_matrix` (982) through 1000. All original
|
||||
seven CONDITIONS and six CATEGORIES (room, queue, EV, staff, goods,
|
||||
business state, represented by the key `business`) remain required.
|
||||
Missing executed check names produce pending/deficits; no fixed-true matrix.
|
||||
- Collector `tools/run_p23_life_observation.py`: `collect_os` (110)
|
||||
invokes `collect_screenshots` (26) at 186. JPEG collection invokes
|
||||
`convert_screenshot` (73) at 55; conversion calls `inspect_conversion`
|
||||
at 107. Raw CUA bytes and tool-call bindings are retained separately from
|
||||
converted PNG and conversion receipts. This wiring was inspected, not run.
|
||||
- `diagnostic_stream.gd` captures hash-addressed `rest_audit` and `motion`
|
||||
at 70–78. `rest_audit` (89) records selection inputs, route/cost probes,
|
||||
locks/transit and comparison-only Runtime output. No stored-rest field was
|
||||
introduced. The source-grounded correction supersedes the old stored-state
|
||||
proposal below. MotionObserver retains two draw frames, not whole Runtime
|
||||
state per frame.
|
||||
- Native route: `tests/ui/test_p23_life_observation.gd` at 18–21 invokes
|
||||
`layout_comparison.gd`; the runner invokes that UI test at 309 for A/B.
|
||||
Layout comparison preloads diagnostics at 5 and starts watching at 65.
|
||||
- Normal OS route is separate from the native UI component test: runner
|
||||
RECORDER at 24 is copied/launched at 145/152; `passive_os_recorder.gd`
|
||||
preloads `passive_observation.gd` at 2 and calls its inherited run at 6.
|
||||
`passive_observation.gd` extends `observation.gd` at 1;
|
||||
`observation.gd` preloads diagnostics/layout comparison at 3–4 and
|
||||
watches the real preview at 26. These fixtures are reached through the
|
||||
OS runner, not directly preloaded by the native UI test.
|
||||
- Existing mutation negatives cover conversion provenance, rest operands,
|
||||
motion history, rectangle/pixel transforms, process/source/profile guards,
|
||||
formal members/distinct PID/ACK, and every condition/category matrix cell.
|
||||
No concrete missing call or negative was found; no code hardening was added.
|
||||
|
||||
## Exact local commands and real results
|
||||
|
||||
- `git status --short`, `git diff --stat`, and targeted `git diff --`
|
||||
for the collector, UI test, diagnostic fixture and marker: inspected existing
|
||||
changes without modifying git state.
|
||||
- Targeted `rg -n` / `rg -n -A...` searches for function definitions,
|
||||
calls, fixture preloads/extends, rest_audit/motion, CONDITIONS/CATEGORIES
|
||||
and unittest methods in the three P23 tools and five P23 fixtures/UI test:
|
||||
wiring results recorded above. No Runtime/source audit or Phase0 repetition.
|
||||
- `python3 -m py_compile tools/run_p23_life_observation.py tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`:
|
||||
initial attempt failed because the interpreter's default cache destination
|
||||
under Library/Caches was not writable; no syntax failure was reported.
|
||||
- `PYTHONPYCACHEPREFIX=docs/plans/original-completion-2026-10-02/execution/p23-03/.finalization-pycache python3 -m py_compile tools/run_p23_life_observation.py tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`:
|
||||
PASS, exit 0 for all three tools.
|
||||
- `python3 -B tools/test_p23_life_observation_evidence.py`:
|
||||
**27 test methods run, 27 passed**, 0 failures/errors; reported 0.044 seconds.
|
||||
These are synthetic checker tests, not native/OS evidence.
|
||||
- `git diff --check`: PASS, exit 0; repeated after this status rewrite.
|
||||
- Read-only `python3 -B -c` JSON/hash/path assertions using
|
||||
`git show 860136f7099f5e3aee99da5798a627a77f2d8eea:<path>`,
|
||||
`git diff --name-only <base>` and
|
||||
`git ls-files --others --exclude-standard`: PASS.
|
||||
All old marker metadata/rows equal base; exactly one appended P23-03
|
||||
required source-authority entry; test SHA
|
||||
`cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1`
|
||||
matches current bytes. suite-policy.json equals base byte-for-byte.
|
||||
All 16 changed/untracked paths relative to base are allowed.
|
||||
- The immutable spec SHA matches
|
||||
`8568457df849ef587c731f072d77244ef5420fc4e1190c5069cfd6cfeea9c89f`
|
||||
using the existing checker's `read_text().strip().encode()` convention.
|
||||
An initial extra raw-byte SHA assertion failed (raw file SHA
|
||||
`1a925e0d7f5c1c9f28670d29dd0fdc5aa48f4d5415946d6de4c15e1d83b5ed66`);
|
||||
normalized verification passed and the spec has no diff against base.
|
||||
- An initial inline validation here-document could not create its shell
|
||||
temporary file; rerun via `python3 -B -c` succeeded.
|
||||
- Removed only the temporary .finalization-pycache tree created by this run.
|
||||
No pre-existing dirty/untracked material was removed.
|
||||
|
||||
## Root work still pending — expected, not worker blockers
|
||||
|
||||
Native Godot execution; actual OS collector with screenshots for both layouts
|
||||
and all six categories, FRONT/BACK and ordinary pan/zoom; formal save and
|
||||
distinct actual PID normal resume; one full exact-candidate **177-suite**
|
||||
regression; independent Opus all-condition review. These must precede any
|
||||
acceptance determination. Constructibility, visual correctness and same-demand
|
||||
improvement have not been claimed from static checks.
|
||||
|
||||
## Superseded history — retained verbatim
|
||||
|
||||
The following earlier blocked statuses (including the 19-method and
|
||||
"not yet wired" claims) describe previous candidates and are superseded by the
|
||||
current wiring and results above.
|
||||
|
||||
# Current RUN continuation — blocked on stored-rest-destination contract
|
||||
|
||||
All seven pre-existing modified files and three untracked fixtures were retained.
|
||||
No git writes, agents, Godot, collector execution, Runner or full regression.
|
||||
This is not a genuine completed submission and makes no acceptance claim.
|
||||
Root native/OS evidence remains **pending**, independently of this source blocker.
|
||||
|
||||
Changes completed in this continuation:
|
||||
- Removed mandatory left-button drag and its pan-change assertion. Camera
|
||||
screenshots now use ordinary-wheel-zoom, pan-mode-button, move-mode-wheel-pan.
|
||||
Existing non-Ctrl event observation is preserved.
|
||||
- Added stdlib PNG decoding with CRC/chunk/end checks, RGB/RGBA and filters 0–4.
|
||||
JPEG dimensions reuse the P08 helper; no JPEG decoder or PIL was added.
|
||||
- Added conversion receipt inspection and Root-only convert_screenshot helper:
|
||||
separate output, original CUA hash/call binding, sips executable SHA/argv,
|
||||
output digests, decoded PNG and magic-derived dimension equality. No actual
|
||||
sips conversion was run here. This helper is not yet wired into collect_os.
|
||||
- Added four image test methods including all filters, CRC corruption, trailing
|
||||
data, converter SHA/argv, raw CUA hash, dimensions and missing-conversion negatives.
|
||||
|
||||
Actually executed: 19 checker unittest methods passed; py_compile passed for
|
||||
the three P23 Python tools. Temporary compilation outputs were removed.
|
||||
Synthetic image tests are checker tests, never native screenshot evidence.
|
||||
|
||||
Concrete contract gap (targeted dependency inspection, not a repeated Phase0 audit):
|
||||
- autoload/simulation_runtime.gd:13109–13125 computes get_staff_rest_report by
|
||||
scanning facilities and selecting a minimum-cost route on every call. The
|
||||
selected facility and eligibility are local variables, not stored state.
|
||||
- autoload/simulation_runtime.gd:8783–8785 calls that query and passes only its
|
||||
cell to advance_staff_commute. At :12656–12690 the movement function either
|
||||
returns without movement or emits a STAFF_MOVED event with a target_cell;
|
||||
it does not persist a selected rest facility ID or eligibility. Movement
|
||||
events cannot supply a current destination for stationary/nonmoving staff.
|
||||
- scripts/staff/staff_runtime.gd:282–315 serializes current_cell, assignment,
|
||||
task and path_handle but no rest destination/eligibility. Runtime snapshot
|
||||
assembly at :511–570 has no stored rest-selection model. PathService's
|
||||
snapshot at scripts/world/path_service.gd:1606 stores path/actor projections,
|
||||
not the selected rest facility and its eligibility.
|
||||
|
||||
Thus recipe (3), "rest destination must equal the typed stored destination and
|
||||
its eligibility; do not re-simulate AI", has no authoritative stored operand.
|
||||
Calling get_staff_rest_report from a fixture would rerun selection, and copying
|
||||
its output into a purported stored-state field would manufacture authority.
|
||||
These Runtime/staff/path files are outside allowed_paths and remain unchanged.
|
||||
|
||||
Read source SHA256:
|
||||
- Runtime: 4696df4a77feb465a5a3ba8b806569f7bcbe1d03c88289acfb78a3928f307d67
|
||||
- StaffRuntime: 807a417bc7866c97cd6d8f5c1368b5480a2d9fd685a2a47a978e81482bf522b5
|
||||
- PathService: 447135729a1106639a6eee6754af3195c8a8b458ca26980ae91fa39d80dcfbd4
|
||||
|
||||
Minimal separate proposal for Root/Opus, not applied: authorize an authoritative
|
||||
rest-decision record at the existing Runtime selection point, including selected
|
||||
facility/cell, eligibility and decision time, with explicit invalidation and
|
||||
snapshot/restore semantics. Alternatively Root must identify an existing typed
|
||||
stored field that has those semantics. No worker relaxation of this gate.
|
||||
|
||||
Remaining source implementation is explicit: collector conversion integration,
|
||||
all row/rectangle bindings, motion history, distinct-PID/formal ACK checker,
|
||||
dynamic matrix and the remaining requested negatives. Existing fail-closed
|
||||
whole-unit behavior remains; no unconditional PASS or new fail stub was added.
|
||||
The 30/88 denominator, seven conditions, six categories and old suite policies
|
||||
are unchanged. Do not begin acceptance assuming only Root evidence is missing.
|
||||
|
||||
---
|
||||
Previous status retained as history:
|
||||
|
||||
# Repair from source 03100be — blocked, implementation incomplete
|
||||
|
||||
Source checkout HEAD confirmed read-only: `03100beabeb55caac03f53db3d1f399b424df517`.
|
||||
Contract base remains `860136f7099f5e3aee99da5798a627a77f2d8eea`.
|
||||
No git writes, Godot, Runner, OS input, regression, or other workers were used.
|
||||
No acceptance or partial credit is claimed. No out-of-scope defect or missing Runtime capability was established.
|
||||
|
||||
Changes in this repair:
|
||||
- Added registered `--verify-only` CLI, concrete missing-index deficits, immutable fixture/spec/contract/baseline/AGENTS pins and a normative P23 plan-section hash. Full-plan SHA is separate provenance only. Existing diagnostic/layout CLI modes retained.
|
||||
- Removed fixture-side begin_leaving/despawn_guest calls; Runtime owns continued guest behavior. Added A/B native entry to the existing component test and updated the existing single marker row SHA.
|
||||
- Added observation.gd extending P08 placement helpers unchanged, with real supply scenario commands/settings, common definition-derived construction budget, seed806042, relocated common cafeteria/kitchen at x6 to avoid declared B elevator footprint, sparse typed captures, normal save/load and whole formal-model comparisons. These construction prechecks have not been executed.
|
||||
- Added passive request/ACK endpoint and recorder for four layout/role combinations, live FRONT/BACK buttons and PanModeButton, ordinary non-Ctrl wheel zoom/pan and ordinary pan drag only. No deferred P23-02 held-gesture credit.
|
||||
- Replaced the collector stub with an adaptation of P08 collect/collect_os using run_all_tests helpers and owned gates, fresh outputs/profiles, native import/parse/layout phases, four OS roles and formal reader copies. Records the current interpreter PIL availability; raw screenshot bytes are left untouched. Collector has not been executed.
|
||||
- Checker now inspects available native/OS streams, comparisons and basic process/source/profile guards, but explicitly fails closed for outstanding whole-unit gates.
|
||||
|
||||
Local verification actually executed: 16 Python unittest cases PASS; ast.parse for the three changed Python tools PASS; one P23 marker row and exact source SHA PASS. No engine or evidence-collector execution.
|
||||
|
||||
Remaining IMPLEMENTATION, separate from missing Root evidence:
|
||||
1. Complete screenshot byte-magic dimensions, stdlib PNG pixel/rect checks, and indexed Root JPEG-to-lossless-PNG conversion binding to raw hash/converter provenance. PIL absence is not waived.
|
||||
2. Add prior-frame visual-position/frame-delta evidence and independent interpolation checks; compare rest destinations with typed stored destination/eligibility without re-simulating AI.
|
||||
3. Bind all request report rows to viewport rectangles/affine and screenshots; current endpoint retains report plus viewport-to-screen transform but does not yet produce every required viewport row rectangle.
|
||||
4. Finish independent distinct-PID/formal-member/resume-state/load-ACK gates. Fixture assertions do not replace checker proof.
|
||||
5. Replace hard failure condition-matrix deficits with exact completed check names only after all seven independent gates exist; finish all requested category and infrastructure negatives. Current check intentionally cannot pass whole-unit integrity.
|
||||
6. Statically review collector lifecycle/failure receipts and fixture category coverage; real constructibility and EV/passenger/queue/goods coverage remain unproved.
|
||||
|
||||
Root native, actual OS for A/B, formal distinct-PID resume, exact-final full regression, independent review, integration/final/ledger: all PENDING. This candidate is blocked on remaining in-scope implementation, not ready for acceptance or a claim that missing Root evidence is the only gap.
|
||||
|
||||
---
|
||||
Previous worker status retained verbatim as history (superseded by the repair status above):
|
||||
|
||||
# P23-03 worker status: submitted static work, implementation incomplete
|
||||
|
||||
Base commit: `860136f7099f5e3aee99da5798a627a77f2d8eea`.
|
||||
This submission preserves the previous dirty work and adds genuine static
|
||||
implementation. It is **not acceptance-ready**. No external Runtime capability
|
||||
gap or out-of-scope defect has been established. Remaining implementation is
|
||||
listed separately from pending Root evidence; no acceptance credit is claimed.
|
||||
|
||||
## Inspection and changes in this repair
|
||||
|
||||
- Re-read the recorded direction, Phase 0 map, previous status and renderer diff.
|
||||
The single frame builder supplies drawing and the deep-copy public report.
|
||||
No renderer redesign or additional renderer was introduced. Existing renderer
|
||||
and component test bytes were retained.
|
||||
- Added `tests/fixtures/p23-03/layout_comparison.gd`. Both layouts initialize the
|
||||
public Runtime with seed 806042, reset budget and clock, identical production
|
||||
defaults and the same recorded 48-person arrival script. Commands build the
|
||||
existing elevator and corridors; B adds only the declared second elevator.
|
||||
Preparation time derives from registered definition build times.
|
||||
- Predeclared metric: sum of destination-arrival minutes for the entire cohort.
|
||||
The checker rejects incomplete cohorts, changed actual arrival payloads,
|
||||
additional layout changes, changed initial state, and non-strict improvement.
|
||||
No observed improvement is claimed: the fixture has not run.
|
||||
- Extended diagnostic_stream with hash-addressed typed event/input records.
|
||||
The fixture records actual command results, domain movement events and
|
||||
per-minute guest/path/EV/facility projections, including service history,
|
||||
queues and density. Whole aggregate state/report pairs are sparse (30-minute
|
||||
intervals); the existing P08 typed encoder is reused without truncation.
|
||||
- Extended independent checking of room service/rescue occupants and business
|
||||
enum fields, queue allocation/overflow, shared transforms, traffic, logistics
|
||||
report selection/order and cabin geometry. Added Rect2 and vector-key decode.
|
||||
The checker still refuses whole-unit acceptance.
|
||||
|
||||
## Exact remaining implementation (not external blockers)
|
||||
|
||||
1. Finish rest destination selection from typed path costs and event/defense
|
||||
locks. Current room check validates eligibility and reported rest references;
|
||||
it does not independently prove which rest destination Runtime selected.
|
||||
2. Finish every actor/passenger mode and geometry, including cleaning,
|
||||
rest/rescue/service slots, visual track history, badge text and screenshot
|
||||
rect/pixel provenance. Cabin endpoints and rectangles are checked, but
|
||||
unpaused prior-frame interpolation origin is not independently established.
|
||||
3. Complete A/B definition/source/production pins and command/event/path
|
||||
cross-validation, invoke the fixture from the Root collector, and add
|
||||
representative real operational scenarios for all six categories and BACK.
|
||||
The added experiment covers the fixed-demand FRONT transport comparison;
|
||||
it does not establish actual service purchasing or all-six-category coverage.
|
||||
4. Replace run_p23_life_observation.py readiness stub with the actual Root
|
||||
collector using P08 owned-process/profile/source/engine/ordinary-audio and
|
||||
CUA request/ACK helpers. It remains unchanged; it launches nothing.
|
||||
Formal save, distinct-PID normal resume, both-layout FRONT/BACK/pan/zoom,
|
||||
complete CUA receipts, all-run cleanup/protection and failure history remain
|
||||
to be wired and independently checked.
|
||||
5. Add missing meaningful negatives, especially normal OS/distinct-PID resume,
|
||||
visual provenance and the remaining rest/motion branches. Existing 12 local
|
||||
tests are not a complete seven-condition test matrix.
|
||||
|
||||
Steps (2) and (3) of the repair direction are therefore still incomplete;
|
||||
step (4) is not implemented. This is a partial static submission, not a claim
|
||||
that the requested repair is finished. Do not begin acceptance on an assumption
|
||||
that all worker deliverables are complete.
|
||||
|
||||
## Checks actually run in this repair
|
||||
|
||||
- `python3 -B tools/test_p23_life_observation_evidence.py`: 12 test methods,
|
||||
zero failures. Synthetic Python examples validate rejection logic only.
|
||||
- AST syntax: all three P23 Python tools parsed.
|
||||
- Marker JSON: old rows and metadata unchanged; exactly one appended row;
|
||||
its SHA matches the unchanged GDScript component test.
|
||||
- `git diff --check`: passed.
|
||||
- No Godot parsing/runtime, native, OS, Runner or full regression execution.
|
||||
- No git writes, subagents, integration, new assets or production changes.
|
||||
suite-policy.json was not changed; no new suite was registered.
|
||||
|
||||
## Source SHA256 at local checks
|
||||
|
||||
| File | SHA256 |
|
||||
| --- | --- |
|
||||
| scenes/main/world_preview.gd | add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0 |
|
||||
| scripts/ui/room_presentation.gd | 55340b7b623532e861be1d21225635fd9169e2e40928597c1cd0458b99524dd9 |
|
||||
| tests/ui/test_p23_life_observation.gd | b9afa66ec73d3a5cadf86f9a71f0d13f5b085c3cc9a534dfe29b2bed0e2e76a0 |
|
||||
| tests/fixtures/p23-03/diagnostic_stream.gd | ff178a094fc310b0640d923a15058c6f6fcf859551c14f6428a5709dcd24260e |
|
||||
| tests/fixtures/p23-03/layout_comparison.gd | ddf00268b8e32e3c5c078ebb57ada321382ecc7c5fbd87efad7e9dfbe768df6b |
|
||||
| tools/run_p23_life_observation.py | 53c2f0638d4a861cf4fcb664504c25df33ae5478207867654f1c8cdf58fa23c7 |
|
||||
| tools/check_p23_life_observation_evidence.py | 318b253a08248a753b88902e382edc5f01cbce6f1ed8092359698b69d3bb0503 |
|
||||
| tools/test_p23_life_observation_evidence.py | 0f20d3de20b44f46f8738c0d022fcc1179656014e85fe31754f00b87ec2c288a |
|
||||
|
||||
## Root evidence: pending
|
||||
|
||||
After genuine completion and freezing: both-layout native and actual normal OS
|
||||
observation, full CUA trace, formal save and distinct-PID resume, mandatory exact
|
||||
version full regression and independent Opus review. These absent Root results
|
||||
are **pending**, not blocked. The fixed 30-package / 88-unit denominator and all
|
||||
P08/P22/P25/Windows/held-gesture/performance exclusions remain unchanged.
|
||||
@@ -0,0 +1,922 @@
|
||||
{
|
||||
"errors": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"expected_row_count": 10,
|
||||
"images": [
|
||||
{
|
||||
"conversion_sha256": "d9e92a5b1925d6e1855f6eaff2820007d7db8dd79a5348ec801a38251b40fe90",
|
||||
"raw_sha256": "ab073ed1055ca2356a050fdd47176c454c109ebe5c008b194ea8a5f149efa7f5",
|
||||
"receipt_sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860",
|
||||
"request_sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167",
|
||||
"whole_image_sha256": "206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a"
|
||||
},
|
||||
{
|
||||
"conversion_sha256": "b25b2224963f9a02ad87675969ab51c90aff29891b136095198afc2fe95acb73",
|
||||
"raw_sha256": "7027a7f1813aa8c9fcff38e6e4b022ed718d6f9c9fd13d31ecc3d6d10818ac10",
|
||||
"receipt_sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860",
|
||||
"request_sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167",
|
||||
"whole_image_sha256": "e6ab48ad864a1b5fdca2da587d95fe7949ac0c59e664a4524db66e3e861a73aa"
|
||||
}
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"rows": [
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0000-image-0.png",
|
||||
"sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0000-image-1.png",
|
||||
"sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286"
|
||||
}
|
||||
],
|
||||
"dimmed": true,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "facility_00000002",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
131.0,
|
||||
388.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 0,
|
||||
"screenshot_rect": [
|
||||
210.0,
|
||||
485.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0001-image-0.png",
|
||||
"sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0001-image-1.png",
|
||||
"sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69"
|
||||
}
|
||||
],
|
||||
"dimmed": true,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "facility_00000003",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
1027.0,
|
||||
388.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 1,
|
||||
"screenshot_rect": [
|
||||
1106.0,
|
||||
485.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0002-image-0.png",
|
||||
"sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0002-image-1.png",
|
||||
"sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "facility_00000007",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
131.0,
|
||||
292.0,
|
||||
128.0,
|
||||
96.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "OPERATING",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 2,
|
||||
"screenshot_rect": [
|
||||
210.0,
|
||||
389.0,
|
||||
128.0,
|
||||
96.0
|
||||
],
|
||||
"state": "OPEN",
|
||||
"state_name": "OPEN",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "queue",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0003-image-0.png",
|
||||
"sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0003-image-1.png",
|
||||
"sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": 36.0,
|
||||
"id": "0",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
259.0,
|
||||
276.0,
|
||||
42.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": null,
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 3,
|
||||
"screenshot_rect": [
|
||||
338.0,
|
||||
373.0,
|
||||
42.0,
|
||||
14.0
|
||||
],
|
||||
"state": {
|
||||
"members": [
|
||||
"guest/guest-auto-00000019"
|
||||
],
|
||||
"overflow": 0,
|
||||
"visible_count": 1
|
||||
},
|
||||
"state_name": null,
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "待ち1人"
|
||||
},
|
||||
{
|
||||
"category": "guest",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0004-image-0.png",
|
||||
"sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0004-image-1.png",
|
||||
"sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "guest/guest-auto-00000019",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
267.0,
|
||||
325.0,
|
||||
48.0,
|
||||
54.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": null,
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 4,
|
||||
"screenshot_rect": [
|
||||
346.0,
|
||||
422.0,
|
||||
48.0,
|
||||
54.0
|
||||
],
|
||||
"state": "QUEUING",
|
||||
"state_name": "QUEUING",
|
||||
"style_colors": [
|
||||
"688da0",
|
||||
"b87659",
|
||||
"a69a63",
|
||||
"89789e",
|
||||
"638d72",
|
||||
"edc49d",
|
||||
"c99371",
|
||||
"a56f50",
|
||||
"303c40",
|
||||
"719d94",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"eee6c7",
|
||||
"e3dfc2",
|
||||
"956940",
|
||||
"d3ad6b",
|
||||
"b48850",
|
||||
"af9059",
|
||||
"d8bc78",
|
||||
"f3ebcf",
|
||||
"efe7ce",
|
||||
"805e44"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_people.gd",
|
||||
"style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "guest",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0005-image-0.png",
|
||||
"sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0005-image-1.png",
|
||||
"sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "guest/guest-auto-00000020",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
139.0,
|
||||
301.0,
|
||||
48.0,
|
||||
54.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": null,
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 5,
|
||||
"screenshot_rect": [
|
||||
218.0,
|
||||
398.0,
|
||||
48.0,
|
||||
54.0
|
||||
],
|
||||
"state": "RECEIVING_SERVICE",
|
||||
"state_name": "RECEIVING_SERVICE",
|
||||
"style_colors": [
|
||||
"688da0",
|
||||
"b87659",
|
||||
"a69a63",
|
||||
"89789e",
|
||||
"638d72",
|
||||
"edc49d",
|
||||
"c99371",
|
||||
"a56f50",
|
||||
"303c40",
|
||||
"719d94",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"eee6c7",
|
||||
"e3dfc2",
|
||||
"956940",
|
||||
"d3ad6b",
|
||||
"b48850",
|
||||
"af9059",
|
||||
"d8bc78",
|
||||
"f3ebcf",
|
||||
"efe7ce",
|
||||
"805e44"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_people.gd",
|
||||
"style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0006-image-0.png",
|
||||
"sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0006-image-1.png",
|
||||
"sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": 73.0,
|
||||
"id": "0",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
133.0,
|
||||
390.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 6,
|
||||
"screenshot_rect": [
|
||||
212.0,
|
||||
487.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "職員不足 未稼働"
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0007-image-0.png",
|
||||
"sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0007-image-1.png",
|
||||
"sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": 73.0,
|
||||
"id": "1",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
1029.0,
|
||||
390.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 7,
|
||||
"screenshot_rect": [
|
||||
1108.0,
|
||||
487.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "職員不足 未稼働"
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0008-image-0.png",
|
||||
"sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0008-image-1.png",
|
||||
"sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": 69.0,
|
||||
"id": "2",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
133.0,
|
||||
294.0,
|
||||
75.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "OPERATING",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 8,
|
||||
"screenshot_rect": [
|
||||
212.0,
|
||||
391.0,
|
||||
75.0,
|
||||
14.0
|
||||
],
|
||||
"state": "OPEN",
|
||||
"state_name": "OPEN",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "営業中 利用1人"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
646334f4ef3c78a8cfe5d68d3738c4d772cdd71cbf80b2dba7c153693ead4419
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,443 @@
|
||||
{
|
||||
"errors": [],
|
||||
"expected_row_count": 4,
|
||||
"images": [
|
||||
{
|
||||
"conversion_sha256": "363c2bdb1da77458226d01e971b9aea1220fcc81f70fb5c3c816a12b7039fac3",
|
||||
"raw_sha256": "2abb4675f9978e7b86599c4334732ebadcb7cd5c5c51deeacaa7d0dee858e9b9",
|
||||
"receipt_sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17",
|
||||
"request_sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093",
|
||||
"whole_image_sha256": "abf7d2e89413431438f35cc3eecb5534a7257cb033906c5397592678b2e9bce4"
|
||||
},
|
||||
{
|
||||
"conversion_sha256": "d9f5e79103258a50bb5d0866725a8488017a9cc8ef150312d16de9043618b55c",
|
||||
"raw_sha256": "e5a7ac0c3ba3f001baae7c771304d1a9a9cf024240176e8f8735a9dcf1091b31",
|
||||
"receipt_sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17",
|
||||
"request_sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093",
|
||||
"whole_image_sha256": "b82e6ef061d9b4e8cbc3321405553f3f447c8abfb61ce38532254130a704ee95"
|
||||
}
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"rows": [
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0000-image-0.png",
|
||||
"sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0000-image-1.png",
|
||||
"sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43"
|
||||
}
|
||||
],
|
||||
"dimmed": true,
|
||||
"eligibility": "visible",
|
||||
"failures": [],
|
||||
"font_width": null,
|
||||
"id": "facility_00000004",
|
||||
"layer": 1,
|
||||
"local_rect": [
|
||||
131.0,
|
||||
388.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 0,
|
||||
"screenshot_rect": [
|
||||
210.0,
|
||||
485.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0001-image-0.png",
|
||||
"sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0001-image-1.png",
|
||||
"sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54"
|
||||
}
|
||||
],
|
||||
"dimmed": true,
|
||||
"eligibility": "visible",
|
||||
"failures": [],
|
||||
"font_width": null,
|
||||
"id": "facility_00000005",
|
||||
"layer": 1,
|
||||
"local_rect": [
|
||||
1027.0,
|
||||
388.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 1,
|
||||
"screenshot_rect": [
|
||||
1106.0,
|
||||
485.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0002-image-0.png",
|
||||
"sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0002-image-1.png",
|
||||
"sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [],
|
||||
"font_width": 73.0,
|
||||
"id": "0",
|
||||
"layer": 1,
|
||||
"local_rect": [
|
||||
133.0,
|
||||
390.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 2,
|
||||
"screenshot_rect": [
|
||||
212.0,
|
||||
487.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "職員不足 未稼働"
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0003-image-0.png",
|
||||
"sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0003-image-1.png",
|
||||
"sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [],
|
||||
"font_width": 73.0,
|
||||
"id": "1",
|
||||
"layer": 1,
|
||||
"local_rect": [
|
||||
1029.0,
|
||||
390.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 3,
|
||||
"screenshot_rect": [
|
||||
1108.0,
|
||||
487.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "職員不足 未稼働"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
f013e0d1d6231833b656859cc73ad0b58b389047e421ad2c43cc598820e21067
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -78,6 +78,7 @@ var _queue_positions: Dictionary = {}
|
||||
var _queue_hidden: Dictionary = {}
|
||||
var _landing_badges: Array[Dictionary] = []
|
||||
var _transport_report: Dictionary = {}
|
||||
var _life_observation: Dictionary = {}
|
||||
var _dragging := false
|
||||
var _drag_button := MOUSE_BUTTON_NONE
|
||||
|
||||
@@ -511,6 +512,7 @@ func _draw() -> void:
|
||||
|
||||
|
||||
func _draw_contents() -> void:
|
||||
_life_observation = {}
|
||||
if _inactive_layer_art != null:
|
||||
_inactive_layer_art.visible = false
|
||||
_draw_background()
|
||||
@@ -521,15 +523,25 @@ func _draw_contents() -> void:
|
||||
var map_rect := get_map_rect()
|
||||
if map_rect.size == Vector2.ZERO:
|
||||
return
|
||||
_life_observation = _build_life_observation()
|
||||
_room_presentations = _life_observation[&"rooms"]
|
||||
_carried_patients = _life_observation[&"carried_patients"]
|
||||
_treated_patients = _life_observation[&"treated_patients"]
|
||||
_resting_staff = _life_observation[&"resting_staff"]
|
||||
_queue_positions = _life_observation[&"queue_positions"]
|
||||
_queue_hidden = _life_observation[&"queue_hidden"]
|
||||
_landing_badges.assign(_life_observation[&"landing_badges"])
|
||||
_transport_report = _life_observation[&"elevators"]
|
||||
_boarded_actor_ids = _life_observation[&"boarded"]
|
||||
_actor_tracks = _life_observation[&"actor_tracks"]
|
||||
_elevator_tracks = _life_observation[&"elevator_tracks"]
|
||||
_draw_world_cells(bounds, map_rect)
|
||||
if show_grid:
|
||||
_draw_congestion_overlay(bounds)
|
||||
_prepare_room_presentations()
|
||||
_prepare_queues()
|
||||
_draw_world_entity_icons()
|
||||
_draw_legacy_stairs()
|
||||
_draw_elevators()
|
||||
_draw_other_layer()
|
||||
_draw_congestion_overlay(bounds)
|
||||
_draw_life_badges()
|
||||
_draw_construction_labels()
|
||||
_draw_facility_labels()
|
||||
_draw_actors()
|
||||
@@ -540,6 +552,102 @@ func _draw_contents() -> void:
|
||||
_draw_map_caption(map_rect)
|
||||
|
||||
|
||||
## The report describes the last submitted draw, not an independently sampled world.
|
||||
## Callers cannot mutate either the frame or its interpolation tracks.
|
||||
func get_life_observation_report() -> Dictionary:
|
||||
return _life_observation.duplicate(true)
|
||||
|
||||
|
||||
## Pure with respect to Runtime and this Control: helpers mutate only fresh local
|
||||
## dictionaries (including copies of visual tracks). No whole Runtime snapshot
|
||||
## is copied per frame; whole typed snapshots belong to sparse diagnostics.
|
||||
func _build_life_observation() -> Dictionary:
|
||||
var model := _observe_rooms()
|
||||
model.merge(_observe_queues())
|
||||
model[&"layer"] = layer_mode
|
||||
model[&"zoom"] = zoom_level
|
||||
model[&"pan"] = pan_offset
|
||||
model[&"viewport"] = size
|
||||
model[&"animation_time"] = _animation_time
|
||||
model[&"actor_tracks"] = _actor_tracks.duplicate(true)
|
||||
model[&"elevator_tracks"] = _elevator_tracks.duplicate(true)
|
||||
model[&"boarded"] = {}
|
||||
var clock := get_node_or_null("/root/GameClock")
|
||||
model[&"paused"] = clock != null and bool(clock.get("paused"))
|
||||
model[&"cabins"] = _observe_elevators(model)
|
||||
model[&"actor_drawings"] = _observe_actors(model)
|
||||
var guests: Array[Dictionary] = []
|
||||
var staff: Array[Dictionary] = []
|
||||
var tasks: Array[Dictionary] = []
|
||||
var guest_service: Variant = _service(&"guest_service")
|
||||
if guest_service != null and guest_service.has_method(&"get_guests"):
|
||||
for guest: GuestRuntime in guest_service.get_guests():
|
||||
guests.append({&"id": guest.guest_id, &"state": GuestRuntime.State.keys()[guest.state],
|
||||
&"cell": guest.current_cell, &"target": guest.target_facility_id,
|
||||
&"service": guest.current_service_id, &"queue": guest.queue_request_id})
|
||||
var staff_service: Variant = _service(&"staff_service")
|
||||
if staff_service != null:
|
||||
for member: StaffRuntime in staff_service.get_staff_members():
|
||||
staff.append({&"id": member.staff_id, &"state": StaffRuntime.State.keys()[member.state],
|
||||
&"cell": member.current_cell, &"task": member.current_task_id,
|
||||
&"facility": member.assigned_facility_id, &"role": member.role_id,
|
||||
&"carried_inventory_id": member.carried_inventory_id})
|
||||
for task: StaffTask in staff_service.get_tasks():
|
||||
tasks.append({&"id": task.task_id, &"state": StaffTask.State.keys()[task.state],
|
||||
&"type": task.task_type, &"staff": task.assigned_staff_id,
|
||||
&"target": task.target_entity_id, &"rescue_facility": task.rescue_facility_id})
|
||||
model[&"guests"] = guests
|
||||
model[&"staff"] = staff
|
||||
model[&"tasks"] = tasks
|
||||
var bridge: Variant = _service(&"supply_logistics_bridge")
|
||||
var goods: Dictionary = bridge.get_report() if bridge != null else {&"requests": [], &"deliveries": []}
|
||||
model[&"goods"] = goods
|
||||
var badges: Array[Dictionary] = []
|
||||
var facilities: Variant = _service(&"facility_service")
|
||||
var world: Variant = _service(&"world_service")
|
||||
if facilities != null and world != null:
|
||||
for instance: FacilityInstance in facilities.get_instances():
|
||||
if instance.origin.z != layer_mode:
|
||||
continue
|
||||
var cells: Array[Vector3i] = world.get_entity_cells(instance.instance_id)
|
||||
if cells.is_empty():
|
||||
continue
|
||||
var rect := _cell_bounds_rect(cells)
|
||||
var data: Dictionary = model[&"rooms"][instance.instance_id]
|
||||
data[&"rect"] = rect
|
||||
data[&"definition_id"] = instance.definition_id
|
||||
var count := RoomPresentation.slots(rect, _cell_dimensions().x / 16.0, instance.definition_id).size()
|
||||
badges.append({&"facility": instance.instance_id, &"kind": &"business",
|
||||
&"at": rect.position + Vector2(2, 2),
|
||||
&"text": RoomPresentation.business_label(data) + " " + RoomPresentation.label(data, count)})
|
||||
var requests: Array[Dictionary] = []
|
||||
for request: Dictionary in goods.get(&"requests", []):
|
||||
if request[&"source_facility_id"] == instance.instance_id or request[&"destination_facility_id"] == instance.instance_id:
|
||||
requests.append(request)
|
||||
if not requests.is_empty():
|
||||
badges.append({&"facility": instance.instance_id, &"kind": &"supply",
|
||||
&"at": rect.position + Vector2(2, 16), &"requests": requests,
|
||||
&"text": RoomPresentation.supply_label(requests)})
|
||||
model[&"badges"] = badges
|
||||
var traffic: Array[Dictionary] = []
|
||||
var path: Variant = _service(&"path_service")
|
||||
var bounds := _world_bounds()
|
||||
if path != null and not bounds.is_empty():
|
||||
for y in range(int(bounds[&"min_y"]), int(bounds[&"max_y"]) + 1):
|
||||
for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1):
|
||||
var cell := Vector3i(x, y, layer_mode)
|
||||
var row: Dictionary = path.get_cell_traffic(cell)
|
||||
if not row.is_empty():
|
||||
traffic.append({&"cell": cell, &"traffic": row, &"rect": get_cell_rect(cell).grow(-2.0)})
|
||||
model[&"traffic"] = traffic
|
||||
return model
|
||||
|
||||
|
||||
func _draw_life_badges() -> void:
|
||||
for badge: Dictionary in _life_observation.get(&"badges", []):
|
||||
_draw_readable_badge(badge[&"at"], badge[&"text"])
|
||||
|
||||
|
||||
func _draw_background() -> void:
|
||||
var night := false
|
||||
var clock := get_node_or_null("/root/GameClock")
|
||||
@@ -650,40 +758,23 @@ func _draw_leaf_cluster(center: Vector2, p: float, variation: int) -> void:
|
||||
draw_rect(Rect2(c+offset,Vector2(3+i%2,2)*p),Color("95ad69") if i%3==0 else Color("648957"))
|
||||
|
||||
|
||||
func _draw_congestion_overlay(bounds: Dictionary) -> void:
|
||||
var path_service: Variant = _service(&"path_service")
|
||||
if path_service == null or not path_service.has_method(&"get_cell_traffic"):
|
||||
return
|
||||
for y in range(int(bounds[&"min_y"]), int(bounds[&"max_y"]) + 1):
|
||||
for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1):
|
||||
var cell := Vector3i(x, y, layer_mode)
|
||||
var traffic_value: Variant = path_service.call(&"get_cell_traffic", cell)
|
||||
if not traffic_value is Dictionary or traffic_value.is_empty():
|
||||
continue
|
||||
var traffic: Dictionary = traffic_value
|
||||
var density := int(traffic.get(&"current_density", 0))
|
||||
var capacity := maxi(int(traffic.get(&"base_capacity", 1)), 1)
|
||||
var blocked := bool(traffic.get(&"temporary_block", false))
|
||||
func _draw_congestion_overlay(_bounds: Dictionary) -> void:
|
||||
for row: Dictionary in _life_observation.get(&"traffic", []):
|
||||
var rect: Rect2 = row[&"rect"]
|
||||
var traffic: Dictionary = row[&"traffic"]
|
||||
var density := int(traffic[&"current_density"])
|
||||
var blocked := bool(traffic[&"temporary_block"])
|
||||
if density <= 0 and not blocked:
|
||||
continue
|
||||
var ratio := float(density) / float(capacity)
|
||||
var color := Color("55c96b")
|
||||
if blocked or ratio > 1.0:
|
||||
color = Color("e34332")
|
||||
elif ratio >= 0.65:
|
||||
color = Color("e3a62f")
|
||||
var rect := get_cell_rect(cell).grow(-2.0)
|
||||
var ratio := float(density) / maxi(int(traffic[&"base_capacity"]), 1)
|
||||
var color := Color("e34332") if blocked or ratio > 1.0 else Color("e3a62f") if ratio >= 0.65 else Color("55c96b")
|
||||
var center := rect.get_center()
|
||||
var route_width := clampf(rect.size.y * 0.16, 2.0, 5.0)
|
||||
draw_line(
|
||||
Vector2(rect.position.x + rect.size.x * 0.12, center.y),
|
||||
draw_line(Vector2(rect.position.x + rect.size.x * 0.12, center.y),
|
||||
Vector2(rect.end.x - rect.size.x * 0.12, center.y),
|
||||
Color(color, 0.90),
|
||||
route_width
|
||||
)
|
||||
Color(color, 0.90 if show_grid else 0.28), clampf(rect.size.y * 0.16, 2.0, 5.0))
|
||||
if blocked:
|
||||
draw_line(rect.position, rect.end, color, 2.0)
|
||||
draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), color, 2.0)
|
||||
draw_line(rect.position, rect.end, Color(color, 0.90 if show_grid else 0.28), 2.0)
|
||||
draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), Color(color, 0.90 if show_grid else 0.28), 2.0)
|
||||
|
||||
|
||||
## Visual-only level comes from the existing live FacilityInstance, not the
|
||||
@@ -794,10 +885,6 @@ func _draw_world_entity_icons() -> void:
|
||||
draw_rect(Rect2(rect.end.x-4*p,rect.position.y+2*p,2*p,2*p),_facility_color(instance_id))
|
||||
if instance_id==selected_entity_id:
|
||||
draw_rect(rect.grow(-p),Color("f5d582"),false,2*p)
|
||||
var data: Dictionary = _room_presentations.get(instance_id, {})
|
||||
if not data.is_empty() and (rect.size.x >= 80 or instance_id == selected_entity_id) and id not in [&"guest_corridor", &"service_corridor", &"guest_stairs", &"service_stairs", &"guest_elevator", &"service_elevator"]:
|
||||
var status := RoomPresentation.label(data, RoomPresentation.slots(rect, p, id).size())
|
||||
_draw_readable_badge(rect.position+Vector2(2, 2), status.replace("利用", "治療") if id == &"infirmary" else status.replace("利用", "休憩") if id in [&"staff_waiting_room", &"staff_dormitory"] else status)
|
||||
|
||||
|
||||
func _draw_readable_badge(at: Vector2, text: String) -> void:
|
||||
@@ -810,13 +897,21 @@ func _draw_readable_badge(at: Vector2, text: String) -> void:
|
||||
|
||||
|
||||
func _prepare_room_presentations() -> void:
|
||||
_room_presentations.clear()
|
||||
_carried_patients.clear()
|
||||
_treated_patients.clear()
|
||||
_resting_staff.clear()
|
||||
var model := _observe_rooms()
|
||||
_room_presentations = model[&"rooms"]
|
||||
_carried_patients = model[&"carried_patients"]
|
||||
_treated_patients = model[&"treated_patients"]
|
||||
_resting_staff = model[&"resting_staff"]
|
||||
|
||||
|
||||
func _observe_rooms() -> Dictionary:
|
||||
var _room_presentations: Dictionary = {}
|
||||
var _carried_patients: Dictionary = {}
|
||||
var _treated_patients: Dictionary = {}
|
||||
var _resting_staff: Dictionary = {}
|
||||
var service: Variant = _service(&"facility_service")
|
||||
if service == null:
|
||||
return
|
||||
return {&"rooms": _room_presentations, &"carried_patients": _carried_patients, &"treated_patients": _treated_patients, &"resting_staff": _resting_staff}
|
||||
for instance: Variant in service.get_instances():
|
||||
if instance.origin.z == layer_mode:
|
||||
_room_presentations[instance.instance_id] = RoomPresentation.describe(instance)
|
||||
@@ -844,12 +939,22 @@ func _prepare_room_presentations() -> void:
|
||||
else:
|
||||
_carried_patients[task.target_entity_id] = task.assigned_staff_id
|
||||
|
||||
return {&"rooms": _room_presentations, &"carried_patients": _carried_patients, &"treated_patients": _treated_patients, &"resting_staff": _resting_staff}
|
||||
|
||||
|
||||
func _prepare_queues() -> void:
|
||||
_queue_positions.clear()
|
||||
_queue_hidden.clear()
|
||||
_landing_badges.clear()
|
||||
_transport_report.clear()
|
||||
var model := _observe_queues()
|
||||
_queue_positions = model[&"queue_positions"]
|
||||
_queue_hidden = model[&"queue_hidden"]
|
||||
_landing_badges.assign(model[&"landing_badges"])
|
||||
_transport_report = model[&"elevators"]
|
||||
|
||||
|
||||
func _observe_queues() -> Dictionary:
|
||||
var _queue_positions: Dictionary = {}
|
||||
var _queue_hidden: Dictionary = {}
|
||||
var _landing_badges: Array[Dictionary] = []
|
||||
var _transport_report: Dictionary = {}
|
||||
var groups: Dictionary = {}
|
||||
var facilities: Variant = _service(&"facility_service")
|
||||
var guests: Variant = _service(&"guest_service")
|
||||
@@ -908,10 +1013,10 @@ func _prepare_queues() -> void:
|
||||
index += 1
|
||||
for hidden: int in range(index, members.size()):
|
||||
_queue_hidden[members[hidden]] = true
|
||||
var text := "待ち%d人" % members.size()
|
||||
if index < members.size():
|
||||
text += "(他%d人)" % (members.size()-index)
|
||||
_landing_badges.append({&"at": get_cell_rect(cell).position + Vector2(-80 if lift_cells.has(cell) else 0, -16), &"text": text})
|
||||
var text := RoomPresentation.queue_label(members.size(), members.size()-index)
|
||||
_landing_badges.append({&"at": get_cell_rect(cell).position + Vector2(-80 if lift_cells.has(cell) else 0, -16), &"text": text, &"cell": cell, &"members": members.duplicate(), &"visible_count": index, &"overflow": members.size()-index})
|
||||
|
||||
return {&"queue_positions": _queue_positions, &"queue_hidden": _queue_hidden, &"landing_badges": _landing_badges, &"elevators": _transport_report}
|
||||
|
||||
|
||||
func _add_queue_member(groups: Dictionary, key: String, cell: Vector3i) -> void:
|
||||
@@ -1039,14 +1144,21 @@ func _draw_facility_labels() -> void:
|
||||
|
||||
|
||||
func _draw_elevators() -> void:
|
||||
_boarded_actor_ids.clear()
|
||||
var transport: Variant = _service(&"elevators")
|
||||
if transport == null or not transport.has_method(&"get_report"):
|
||||
_elevator_tracks.clear()
|
||||
return
|
||||
var report_value: Variant = _transport_report
|
||||
if not report_value is Dictionary:
|
||||
return
|
||||
for row: Dictionary in _life_observation.get(&"cabins", []):
|
||||
PixelArt.elevator_cabin(self, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"])
|
||||
for actor: Dictionary in row[&"passengers"]:
|
||||
_draw_observed_actor(actor)
|
||||
PixelArt.elevator_gate(self, row[&"rect"], row[&"pixel"], row[&"doors_open"])
|
||||
_draw_readable_badge(row[&"badge_at"], row[&"badge"])
|
||||
for badge: Dictionary in _life_observation.get(&"landing_badges", []):
|
||||
_draw_readable_badge(badge[&"at"], badge[&"text"])
|
||||
|
||||
|
||||
func _observe_elevators(observation: Dictionary) -> Array[Dictionary]:
|
||||
var cabins: Array[Dictionary] = []
|
||||
var _boarded_actor_ids: Dictionary = observation[&"boarded"]
|
||||
var _elevator_tracks: Dictionary = observation[&"elevator_tracks"]
|
||||
var report_value: Dictionary = observation[&"elevators"]
|
||||
var alive: Dictionary = {}
|
||||
var p := _cell_dimensions().x/16.0
|
||||
for value: Variant in report_value.get(&"shafts", []):
|
||||
@@ -1062,7 +1174,7 @@ func _draw_elevators() -> void:
|
||||
for actor_id: Variant in passengers:
|
||||
_boarded_actor_ids[_elevator_actor_key(String(actor_id))] = true
|
||||
var floor_value := float(shaft.get(&"visual_floor", shaft.get(&"current_floor", stops[0].y)))
|
||||
var visual_floor := _tracked_elevator_floor(shaft_id, floor_value)
|
||||
var visual_floor := _tracked_elevator_floor(shaft_id, floor_value, observation)
|
||||
if stops[0].z != layer_mode:
|
||||
continue
|
||||
var origin := get_cell_rect(stops[0])
|
||||
@@ -1074,7 +1186,7 @@ func _draw_elevators() -> void:
|
||||
var operational := bool(shaft.get(&"operational", true))
|
||||
var phase := StringName(shaft.get(&"phase", &"IDLE"))
|
||||
var doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"]
|
||||
PixelArt.elevator_cabin(self,cabin,p,layer_mode==LayerMode.BACK,operational)
|
||||
var passengers_drawn: Array[Dictionary] = []
|
||||
var seat_columns := maxi(int((cabin.size.x / p - 4.0) / 8.0), 1)
|
||||
var visible_count := mini(passengers.size(), seat_columns*2)
|
||||
for index: int in range(visible_count):
|
||||
@@ -1090,22 +1202,19 @@ func _draw_elevators() -> void:
|
||||
var x := cabin.position.x + (2.0 + (index % seat_columns + 0.5)*8.0)*p
|
||||
var feet := cabin.end.y - (2.0 + int(index / seat_columns)*9.0)*p
|
||||
var center := Vector2(x, feet+3*p-_cell_dimensions().y*0.5)
|
||||
_draw_pixel_actor(center,actor_key,is_staff,state,false,carrying,&"",true)
|
||||
PixelArt.elevator_gate(self,cabin,p,doors_open)
|
||||
var capacity := int(shaft.get(&"capacity", 0))
|
||||
var load_text := "%d/%d人" % [passengers.size(), capacity]
|
||||
if capacity > 0 and passengers.size() >= capacity:
|
||||
load_text = "満員 " + load_text
|
||||
if not operational:
|
||||
load_text = "停止 " + load_text
|
||||
_draw_readable_badge(Vector2(cabin.position.x, cabin.position.y-14), load_text)
|
||||
for badge: Dictionary in _landing_badges:
|
||||
_draw_readable_badge(badge[&"at"], badge[&"text"])
|
||||
passengers_drawn.append(_actor_drawing(center,actor_key,is_staff,state,false,carrying,&"",true,observation))
|
||||
cabins.append({&"shaft_id": shaft_id, &"rect": cabin, &"pixel": p,
|
||||
&"operational": operational, &"doors_open": doors_open,
|
||||
&"passengers": passengers_drawn,
|
||||
&"badge_at": Vector2(cabin.position.x, cabin.position.y-14),
|
||||
&"badge": RoomPresentation.elevator_label(shaft)})
|
||||
|
||||
for shaft_id: Variant in _elevator_tracks.keys():
|
||||
if not alive.has(shaft_id):
|
||||
_elevator_tracks.erase(shaft_id)
|
||||
|
||||
return cabins
|
||||
|
||||
|
||||
func _elevator_actor_key(actor_id: String) -> String:
|
||||
if actor_id.begins_with("guest:"):
|
||||
@@ -1115,7 +1224,8 @@ func _elevator_actor_key(actor_id: String) -> String:
|
||||
return actor_id
|
||||
|
||||
|
||||
func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> float:
|
||||
func _tracked_elevator_floor(shaft_id: StringName, current_floor: float, observation: Dictionary = {}) -> float:
|
||||
var _elevator_tracks: Dictionary = observation.get(&"elevator_tracks", self._elevator_tracks)
|
||||
if not _elevator_tracks.has(shaft_id):
|
||||
_elevator_tracks[shaft_id] = {&"floor":current_floor,&"from":current_floor,&"at":_animation_time}
|
||||
var track: Dictionary = _elevator_tracks[shaft_id]
|
||||
@@ -1131,6 +1241,19 @@ func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> floa
|
||||
|
||||
|
||||
func _draw_actors() -> void:
|
||||
for row: Dictionary in _life_observation.get(&"actor_drawings", []):
|
||||
_draw_observed_actor(row)
|
||||
|
||||
|
||||
func _observe_actors(observation: Dictionary) -> Array[Dictionary]:
|
||||
var rows: Array[Dictionary] = []
|
||||
var _actor_tracks: Dictionary = observation[&"actor_tracks"]
|
||||
var _boarded_actor_ids: Dictionary = observation[&"boarded"]
|
||||
var _queue_hidden: Dictionary = observation[&"queue_hidden"]
|
||||
var _queue_positions: Dictionary = observation[&"queue_positions"]
|
||||
var _carried_patients: Dictionary = observation[&"carried_patients"]
|
||||
var _treated_patients: Dictionary = observation[&"treated_patients"]
|
||||
var _resting_staff: Dictionary = observation[&"resting_staff"]
|
||||
var counts: Dictionary = {}
|
||||
var alive: Dictionary = {}
|
||||
var service_slots: Dictionary = {}
|
||||
@@ -1144,18 +1267,18 @@ func _draw_actors() -> void:
|
||||
if _boarded_actor_ids.has(key) or _queue_hidden.has(key):
|
||||
_actor_tracks.erase(key)
|
||||
continue
|
||||
var position := _tracked_actor_position(key,guest.current_cell)
|
||||
var position := _tracked_actor_position(key,guest.current_cell,observation)
|
||||
if guest.current_cell.z==layer_mode:
|
||||
var receiving := not String(guest.current_service_id).is_empty()
|
||||
if _queue_positions.has(key):
|
||||
position = _queue_positions[key]
|
||||
elif receiving:
|
||||
position = _service_use_position(guest,position,service_slots)
|
||||
position = _service_use_position(guest,position,service_slots,observation)
|
||||
if not position.is_finite():
|
||||
continue
|
||||
else:
|
||||
position += _actor_offset(guest.current_cell,counts)
|
||||
_draw_pixel_actor(position,key,false,int(guest.state),receiving,false,_guest_activity(guest))
|
||||
rows.append(_actor_drawing(position,key,false,int(guest.state),receiving,false,_guest_activity(guest),false,observation))
|
||||
var staff_service: Variant = _service(&"staff_service")
|
||||
if staff_service != null and staff_service.has_method(&"get_staff_members"):
|
||||
for staff: Variant in staff_service.call(&"get_staff_members"):
|
||||
@@ -1166,27 +1289,29 @@ func _draw_actors() -> void:
|
||||
if _boarded_actor_ids.has(key) or _queue_hidden.has(key):
|
||||
_actor_tracks.erase(key)
|
||||
continue
|
||||
var position := _tracked_actor_position(key,staff.current_cell)
|
||||
var position := _tracked_actor_position(key,staff.current_cell,observation)
|
||||
if staff.current_cell.z==layer_mode:
|
||||
if _carried_patients.has(staff.staff_id):
|
||||
var carrier: Variant = staff_service.get_staff(_carried_patients[staff.staff_id])
|
||||
if carrier != null:
|
||||
position = _tracked_actor_position("staff/"+String(carrier.staff_id), carrier.current_cell) - Vector2(0, 12*_cell_dimensions().x/16.0)
|
||||
_draw_pixel_actor(position,key,true,int(staff.state),false,false)
|
||||
position = _tracked_actor_position("staff/"+String(carrier.staff_id), carrier.current_cell,observation) - Vector2(0, 12*_cell_dimensions().x/16.0)
|
||||
rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
|
||||
continue
|
||||
if _resting_staff.has(staff.staff_id):
|
||||
position = _resting_staff_position(staff.staff_id, position)
|
||||
if position.is_finite(): _draw_pixel_actor(position,key,true,int(staff.state),false,false)
|
||||
position = _resting_staff_position(staff.staff_id, position,observation)
|
||||
if position.is_finite(): rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
|
||||
continue
|
||||
if _treated_patients.has(staff.staff_id):
|
||||
position = _rescue_treatment_position(staff.staff_id, position)
|
||||
if position.is_finite(): _draw_pixel_actor(position,key,true,int(staff.state),false,false)
|
||||
position = _rescue_treatment_position(staff.staff_id, position,observation)
|
||||
if position.is_finite(): rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation))
|
||||
continue
|
||||
_draw_pixel_actor(_queue_positions.get(key, _staff_work_position(staff, position+_actor_offset(staff.current_cell,counts))),key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff))
|
||||
rows.append(_actor_drawing(_queue_positions.get(key, _staff_work_position(staff, position+_actor_offset(staff.current_cell,counts),observation)),key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff),false,observation))
|
||||
for key: Variant in _actor_tracks.keys():
|
||||
if not alive.has(key):
|
||||
_actor_tracks.erase(key)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
func _staff_work_animation(staff: StaffRuntime) -> StringName:
|
||||
if staff.role_id != &"cleaner":
|
||||
@@ -1201,7 +1326,8 @@ func _staff_work_animation(staff: StaffRuntime) -> StringName:
|
||||
return &"idle"
|
||||
|
||||
|
||||
func _staff_work_position(staff: Variant, fallback: Vector2) -> Vector2:
|
||||
func _staff_work_position(staff: Variant, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
|
||||
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
|
||||
if staff.state not in [StaffRuntime.State.WORKING, StaffRuntime.State.AVAILABLE]:
|
||||
return fallback
|
||||
var service: Variant = _service(&"facility_service")
|
||||
@@ -1227,7 +1353,8 @@ func _staff_work_position(staff: Variant, fallback: Vector2) -> Vector2:
|
||||
return Vector2(rect.position.x+(slot+0.5)*rect.size.x/columns, rect.position.y+26*p-_cell_dimensions().y*0.5)
|
||||
|
||||
|
||||
func _tracked_actor_position(key: String, cell: Vector3i) -> Vector2:
|
||||
func _tracked_actor_position(key: String, cell: Vector3i, observation: Dictionary = {}) -> Vector2:
|
||||
var _actor_tracks: Dictionary = observation.get(&"actor_tracks", self._actor_tracks)
|
||||
if not _actor_tracks.has(key):
|
||||
_actor_tracks[key] = {&"cell":cell,&"from":cell,&"at":_animation_time,&"direction":1.0}
|
||||
var track: Dictionary = _actor_tracks[key]
|
||||
@@ -1246,7 +1373,8 @@ func _tracked_actor_position(key: String, cell: Vector3i) -> Vector2:
|
||||
return cell_to_canvas(track[&"from"]).lerp(cell_to_canvas(cell),t)
|
||||
|
||||
|
||||
func _service_use_position(guest: Variant, fallback: Vector2, _slots: Dictionary) -> Vector2:
|
||||
func _service_use_position(guest: Variant, fallback: Vector2, _slots: Dictionary, observation: Dictionary = {}) -> Vector2:
|
||||
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
|
||||
# Guest paths stop at an access cell. While a service is active, project the
|
||||
# occupant into that facility's use position; authoritative paths stay intact.
|
||||
var id := StringName(guest.target_facility_id)
|
||||
@@ -1281,6 +1409,19 @@ func _guest_activity(guest: Variant) -> StringName:
|
||||
|
||||
|
||||
func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false) -> void:
|
||||
_draw_observed_actor(_actor_drawing(center, key, staff, state, receiving, carrying, activity, riding))
|
||||
|
||||
|
||||
func _draw_observed_actor(row: Dictionary) -> void:
|
||||
var at: Vector2 = row[&"at"]
|
||||
var p: float = row[&"scale"]
|
||||
draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
|
||||
People.draw(self, at, p, row[&"mode"], row[&"frame"], row[&"variant"], row[&"staff"], row[&"flip"])
|
||||
|
||||
|
||||
func _actor_drawing(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false, observation: Dictionary = {}) -> Dictionary:
|
||||
var _actor_tracks: Dictionary = observation.get(&"actor_tracks", self._actor_tracks)
|
||||
var _queue_positions: Dictionary = observation.get(&"queue_positions", self._queue_positions)
|
||||
var world_pixel := _cell_dimensions().x / 16.0
|
||||
var scale := 0.5 if riding else 0.75
|
||||
var p := maxf(floorf(world_pixel * scale), 1.0) if world_pixel >= 2.0 else world_pixel * scale
|
||||
@@ -1307,8 +1448,10 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re
|
||||
elif _queue_positions.has(key) or (not staff and state == GuestRuntime.State.QUEUING):
|
||||
mode = &"wait"
|
||||
var at := (center + Vector2(-8 * p, _cell_dimensions().y * 0.5 - 3 * world_pixel - 17 * p)).round()
|
||||
draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2))
|
||||
People.draw(self, at, p, mode, frame, variant, staff, float(track.get(&"direction", 1.0)) < 0.0)
|
||||
return {&"key": key, &"center": center, &"at": at, &"scale": p, &"mode": mode,
|
||||
&"frame": frame, &"variant": variant, &"staff": staff, &"state": state,
|
||||
&"receiving": receiving, &"carrying": carrying, &"activity": activity,
|
||||
&"riding": riding, &"flip": float(track.get(&"direction", 1.0)) < 0.0}
|
||||
|
||||
|
||||
func _draw_excavation_jobs() -> void:
|
||||
@@ -1421,14 +1564,14 @@ func _facility_color(instance_id: StringName) -> Color:
|
||||
var instance: Variant = facility_service.call(&"get_instance", instance_id)
|
||||
if instance == null:
|
||||
return Color("6f8b67") if layer_mode == LayerMode.FRONT else Color("427582")
|
||||
match int(instance.state):
|
||||
0:
|
||||
match int(_room_presentations.get(instance_id, {}).get(&"state", instance.state)):
|
||||
FacilityInstance.State.BROKEN:
|
||||
return Color("a24b45")
|
||||
1, 2, 3:
|
||||
FacilityInstance.State.NO_ACCESS, FacilityInstance.State.NO_POWER, FacilityInstance.State.NO_STAFF:
|
||||
return Color("a77d3f")
|
||||
4:
|
||||
FacilityInstance.State.CLOSED:
|
||||
return Color("6b6258")
|
||||
5:
|
||||
FacilityInstance.State.DEGRADED:
|
||||
return Color("89914d")
|
||||
_:
|
||||
return Color("6f9a55") if layer_mode == LayerMode.FRONT else Color("67518e")
|
||||
@@ -1579,7 +1722,9 @@ func _normalized_rotation(value: int) -> int:
|
||||
return posmod(value, 360) / 90 * 90
|
||||
|
||||
|
||||
func _rescue_treatment_position(patient_id: StringName, fallback: Vector2) -> Vector2:
|
||||
func _rescue_treatment_position(patient_id: StringName, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
|
||||
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
|
||||
var _treated_patients: Dictionary = observation.get(&"treated_patients", self._treated_patients)
|
||||
var facility_id: StringName = _treated_patients[patient_id]
|
||||
var world: Variant = _service(&"world_service")
|
||||
var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else []
|
||||
@@ -1592,7 +1737,9 @@ func _rescue_treatment_position(patient_id: StringName, fallback: Vector2) -> Ve
|
||||
return anchors[slot]+Vector2(0,-2*p-_cell_dimensions().y*0.5)
|
||||
|
||||
|
||||
func _resting_staff_position(staff_id: StringName, fallback: Vector2) -> Vector2:
|
||||
func _resting_staff_position(staff_id: StringName, fallback: Vector2, observation: Dictionary = {}) -> Vector2:
|
||||
var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations)
|
||||
var _resting_staff: Dictionary = observation.get(&"resting_staff", self._resting_staff)
|
||||
var facility_id: StringName = _resting_staff[staff_id]
|
||||
var world: Variant = _service(&"world_service")
|
||||
var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else []
|
||||
|
||||
@@ -10,7 +10,9 @@ static func describe(instance: Variant) -> Dictionary:
|
||||
if not ids.has(id):
|
||||
ids.append(id)
|
||||
ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
return {&"operation": int(instance.operation_state), &"people": ids,
|
||||
return {&"state": int(instance.state), &"state_name": FacilityInstance.State.keys()[instance.state],
|
||||
&"reasons": instance.state_reasons.duplicate(),
|
||||
&"operation": int(instance.operation_state), &"people": ids,
|
||||
&"count": ids.size(), &"limit": int(instance.customer_capacity),
|
||||
&"staff": instance.active_staff_ids.duplicate(), &"suspension": StringName(instance.suspension_reason)}
|
||||
|
||||
@@ -50,3 +52,48 @@ static func label(data: Dictionary, visible_count: int) -> String:
|
||||
if count > visible_count:
|
||||
value += "(他%d人)" % (count - visible_count)
|
||||
return value
|
||||
|
||||
|
||||
static func business_label(data: Dictionary) -> String:
|
||||
if data.is_empty():
|
||||
return ""
|
||||
if int(data[&"operation"]) == FacilityInstance.OperationState.SUSPENDED:
|
||||
return "休止・摘発" if data[&"suspension"] == &"RAIDED" else "休止・破壊"
|
||||
match int(data[&"state"]):
|
||||
FacilityInstance.State.BROKEN: return "破損"
|
||||
FacilityInstance.State.NO_ACCESS: return "通路なし"
|
||||
FacilityInstance.State.NO_POWER: return "魔力なし"
|
||||
FacilityInstance.State.NO_STAFF: return "職員不足"
|
||||
FacilityInstance.State.CLOSED: return "営業停止"
|
||||
FacilityInstance.State.DEGRADED: return "低下運転"
|
||||
FacilityInstance.State.OPEN: return "営業中"
|
||||
return String(data[&"state_name"])
|
||||
|
||||
|
||||
static func queue_label(count: int, overflow: int) -> String:
|
||||
return "待ち%d人" % count + ("(他%d人)" % overflow if overflow > 0 else "")
|
||||
|
||||
|
||||
static func elevator_label(shaft: Dictionary) -> String:
|
||||
var count := (shaft[&"boarded"] as Array).size()
|
||||
var capacity := int(shaft[&"capacity"])
|
||||
var value := "%d/%d人" % [count, capacity]
|
||||
if capacity > 0 and count >= capacity:
|
||||
value = "満員 " + value
|
||||
if not bool(shaft[&"operational"]):
|
||||
value = "停止 " + value
|
||||
return value
|
||||
|
||||
|
||||
static func supply_label(requests: Array[Dictionary]) -> String:
|
||||
# These are requests, never invented stock quantities. Preserve actual states.
|
||||
var states: Dictionary = {}
|
||||
for request: Dictionary in requests:
|
||||
var state := String(request[&"state"])
|
||||
states[state] = int(states.get(state, 0)) + 1
|
||||
var names: Array = states.keys()
|
||||
names.sort()
|
||||
var parts: PackedStringArray = []
|
||||
for state: String in names:
|
||||
parts.append("%s:%d" % [state, states[state]])
|
||||
return "物資依頼 " + " / ".join(parts)
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
extends RefCounted
|
||||
## Sparse whole typed captures. No acceptance flags and no truncation.
|
||||
const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd")
|
||||
var directory: String
|
||||
var manifest: FileAccess
|
||||
var observer: MotionObserver
|
||||
|
||||
class MotionObserver extends Node:
|
||||
var view: WorldPreview
|
||||
var frames: Array = []
|
||||
var elapsed := 0.0
|
||||
var scaled_elapsed := 0.0
|
||||
func _ready() -> void:
|
||||
RenderingServer.frame_post_draw.connect(after_draw)
|
||||
view.draw.connect(on_view_draw)
|
||||
func _process(delta: float) -> void:
|
||||
elapsed += delta
|
||||
var clock := get_node_or_null("/root/GameClock")
|
||||
if clock == null or not bool(clock.get("paused")):
|
||||
scaled_elapsed += delta * maxf(float(clock.get("speed")) if clock != null else 1.0, 0.0)
|
||||
func on_view_draw() -> void:
|
||||
# Read the draw-consumed report after the real _draw completes.
|
||||
after_draw.call_deferred()
|
||||
func display_sample(report: Dictionary) -> Dictionary:
|
||||
var sample := {}
|
||||
for key: String in ["animation_time", "paused", "layer", "zoom", "pan", "viewport", "actor_tracks", "elevator_tracks", "actor_drawings", "cabins"]:
|
||||
sample[key] = report[key]
|
||||
return sample
|
||||
func after_draw() -> void:
|
||||
if not is_instance_valid(view): return
|
||||
var frame := Engine.get_process_frames()
|
||||
if not frames.is_empty() and frame <= int(frames[-1]["frame"]): return
|
||||
var report := view.get_life_observation_report()
|
||||
if report.is_empty(): return
|
||||
var sample := display_sample(report)
|
||||
if not frames.is_empty() and sample == frames[-1]["display"] and not sample["paused"]: return
|
||||
# Keep only two draw frames, never whole Runtime copies per frame.
|
||||
frames.append({"frame": frame, "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample})
|
||||
if frames.size() > 2: frames.pop_front()
|
||||
elapsed = 0.0
|
||||
scaled_elapsed = 0.0
|
||||
func _exit_tree() -> void:
|
||||
if is_instance_valid(view) and view.draw.is_connected(on_view_draw): view.draw.disconnect(on_view_draw)
|
||||
if RenderingServer.frame_post_draw.is_connected(after_draw): RenderingServer.frame_post_draw.disconnect(after_draw)
|
||||
|
||||
func watch(view: WorldPreview) -> void:
|
||||
observer = MotionObserver.new()
|
||||
observer.view = view
|
||||
view.add_child(observer)
|
||||
|
||||
func await_draws(tree: SceneTree, view: WorldPreview, n: int = 2, bound: int = 120) -> bool:
|
||||
var start := Engine.get_process_frames()
|
||||
for unused: int in range(bound):
|
||||
view.queue_redraw()
|
||||
await tree.process_frame
|
||||
if not is_instance_valid(observer): break
|
||||
var report := view.get_life_observation_report()
|
||||
if observer.frames.size() == n and not report.is_empty() and int(observer.frames[0]["frame"]) > start and observer.frames[-1]["display"] == observer.display_sample(report):
|
||||
return true
|
||||
# Persist timeout as a failure, never a skip.
|
||||
record("draw-history-timeout", {"start_frame": start, "bound": bound, "required_frames": n})
|
||||
return false
|
||||
|
||||
func open(path: String) -> bool:
|
||||
directory = path
|
||||
if DirAccess.dir_exists_absolute(path):
|
||||
return false
|
||||
if DirAccess.make_dir_recursive_absolute(path.path_join("objects")) != OK:
|
||||
return false
|
||||
manifest = FileAccess.open(path.path_join("manifest.jsonl"), FileAccess.WRITE)
|
||||
return manifest != null
|
||||
|
||||
func object(value: Variant) -> String:
|
||||
var text := JSON.stringify(Codec.new().typed(value)) + "\n"
|
||||
var digest := text.sha256_text()
|
||||
var path := directory.path_join("objects").path_join(digest + ".json")
|
||||
if FileAccess.file_exists(path):
|
||||
return digest if FileAccess.get_sha256(path) == digest else ""
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return ""
|
||||
file.store_string(text)
|
||||
file.close()
|
||||
return digest if FileAccess.get_sha256(path) == digest else ""
|
||||
|
||||
func capture(label: String, runtime: Variant, view: WorldPreview) -> bool:
|
||||
if manifest == null:
|
||||
return false
|
||||
var state: Dictionary = runtime.create_snapshot()
|
||||
var report := view.get_life_observation_report()
|
||||
var state_hash := object(state)
|
||||
var report_hash := object(report)
|
||||
var audit_hash := object(rest_audit(runtime))
|
||||
var motion_hash := object({"frames": observer.frames.duplicate(true) if is_instance_valid(observer) else []})
|
||||
var after: Dictionary = runtime.create_snapshot()
|
||||
var after_hash := object(after)
|
||||
if state_hash.is_empty() or report_hash.is_empty() or audit_hash.is_empty():
|
||||
return false
|
||||
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
|
||||
"ticks_usec": Time.get_ticks_usec(), "state": state_hash, "report": report_hash,
|
||||
"rest_audit": audit_hash, "motion": motion_hash, "state_after": after_hash,
|
||||
"input_method": "diagnostic", "acceptance": "pending"}))
|
||||
manifest.flush()
|
||||
return var_to_bytes(state) == var_to_bytes(after)
|
||||
|
||||
func close() -> void:
|
||||
if manifest != null:
|
||||
manifest.close()
|
||||
manifest = null
|
||||
|
||||
# Public selection inputs; Runtime output is only a comparison operand.
|
||||
func rest_audit(runtime: Variant) -> Dictionary:
|
||||
var rows: Array = []
|
||||
for member: StaffRuntime in runtime.staff_service.get_staff_members():
|
||||
var memberships: Array = []
|
||||
for raid_id: StringName in runtime.defense_service.get_raid_ids():
|
||||
var raid: Dictionary = runtime.defense_service.get_raid(raid_id)
|
||||
if raid.get(&"combatants", {}).has(member.staff_id):
|
||||
memberships.append({"raid_id": raid_id, "state": DefenseService.RaidState.keys()[raid[&"state"]],
|
||||
"side": raid[&"combatants"][member.staff_id].get(&"side", &"")})
|
||||
var facilities: Array = []
|
||||
for room: FacilityInstance in runtime.facility_service.get_instances():
|
||||
var probes: Array = []
|
||||
var candidates: Dictionary = {}
|
||||
var occupied := room.occupied_cells.duplicate()
|
||||
if occupied.is_empty(): occupied.append(room.origin)
|
||||
for cell: Vector3i in occupied:
|
||||
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
|
||||
var present: bool = runtime.path_service.has_cell(cell + delta)
|
||||
probes.append({"cell": cell + delta, "has_cell": present})
|
||||
if present: candidates[cell + delta] = true
|
||||
var ordered: Array = candidates.keys()
|
||||
ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool:
|
||||
if a.z != b.z: return a.z < b.z
|
||||
if a.y != b.y: return a.y < b.y
|
||||
return a.x < b.x)
|
||||
var routes: Array = []
|
||||
for cell: Vector3i in ordered:
|
||||
var path: Array = runtime.path_service.find_path(member.current_cell, cell,
|
||||
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
|
||||
var edges: Array = []
|
||||
for index: int in range(1, path.size()):
|
||||
# Empty reports remain explicit mismatch evidence.
|
||||
edges.append(runtime.path_service.get_edge_report(path[index - 1], path[index], [&"staff"]))
|
||||
routes.append({"cell": cell, "path": path, "edges": edges,
|
||||
"calculate_path_cost": runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1})
|
||||
facilities.append({"instance_id": room.instance_id, "definition_id": room.definition_id,
|
||||
"operation_state": FacilityInstance.OperationState.keys()[room.operation_state],
|
||||
"origin": room.origin, "occupied_cells": room.occupied_cells.duplicate(),
|
||||
"probes": probes, "ordered": ordered, "routes": routes})
|
||||
rows.append({"staff_id": member.staff_id, "state": StaffRuntime.State.keys()[member.state],
|
||||
"health": member.health, "current_task_id": member.current_task_id, "current_cell": member.current_cell,
|
||||
"defense_memberships": memberships, "actor_lock": runtime.event_service.get_actor_locks().has(member.staff_id),
|
||||
"in_transit": runtime.is_staff_in_transit(member.staff_id), "facilities": facilities,
|
||||
"comparison_only": runtime.get_staff_rest_report(member.staff_id)})
|
||||
return {"staff": rows, "path_service_sha256": FileAccess.get_sha256("res://scripts/world/path_service.gd")}
|
||||
|
||||
func record(label: String, value: Dictionary) -> bool:
|
||||
if manifest == null: return false
|
||||
var digest := object(value)
|
||||
if digest.is_empty(): return false
|
||||
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
|
||||
"ticks_usec": Time.get_ticks_usec(), "object": digest}))
|
||||
manifest.flush()
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://d23lifestr03
|
||||
@@ -0,0 +1,102 @@
|
||||
extends RefCounted
|
||||
## Public-command experiment; no synthetic state, costs or capacities.
|
||||
## Predeclared metric: total destination-arrival minutes of the entire cohort.
|
||||
## B must be strictly smaller. Incomplete cohorts cannot establish improvement.
|
||||
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
|
||||
const SEED := 806042
|
||||
const WINDOW := 480
|
||||
const START := Vector3i(2, 0, 0)
|
||||
const GOAL := Vector3i(2, 1, 0)
|
||||
var stream: RefCounted
|
||||
|
||||
func declaration() -> Dictionary:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = SEED
|
||||
var arrivals: Array[Dictionary] = []
|
||||
for index: int in range(48):
|
||||
arrivals.append({&"id": StringName("p23-%03d" % index),
|
||||
&"offset": (index / 4) * 15 + rng.randi_range(0, 2)})
|
||||
arrivals.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a[&"offset"] < b[&"offset"] or (a[&"offset"] == b[&"offset"] and String(a[&"id"]) < String(b[&"id"])))
|
||||
return {&"seed": SEED, &"initial_clock": [1, 6, 0], &"budget": &"GameState.reset",
|
||||
&"production": &"Runtime.initialize defaults; production operations not enabled",
|
||||
&"arrivals": arrivals, &"from": START, &"to": GOAL, &"window": WINDOW,
|
||||
&"metric": &"sum_destination_arrival_minutes_entire_cohort",
|
||||
&"improvement": {&"definition_id": &"guest_elevator", &"origin": Vector3i(4, 0, 0)}}
|
||||
|
||||
func run(tree: SceneTree, directory: String, layout: String) -> bool:
|
||||
if layout not in ["A", "B"]: return false
|
||||
var global_clock := tree.root.get_node_or_null("GameClock")
|
||||
if global_clock != null: global_clock.set_paused(true)
|
||||
stream = Stream.new()
|
||||
if not stream.open(directory): return false
|
||||
var input := declaration()
|
||||
if not stream.record("declaration", input):
|
||||
stream.close()
|
||||
return false
|
||||
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
|
||||
var clock: Variant = load("res://autoload/game_clock.gd").new()
|
||||
var state: Variant = load("res://autoload/game_state.gd").new()
|
||||
clock.reset(1, 6, 0)
|
||||
state.reset()
|
||||
var ok: bool = runtime.initialize(clock, state, SEED)[&"valid"]
|
||||
clock.set_paused(true)
|
||||
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
ok = stream.record("initial", runtime.create_snapshot()) and ok
|
||||
var builds: Array = [[&"guest_elevator", Vector3i.ZERO],
|
||||
[&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)]]
|
||||
if layout == "B": builds.append([input[&"improvement"][&"definition_id"], input[&"improvement"][&"origin"]])
|
||||
var construction_minutes := 0
|
||||
# Identical preparation time derived from the same registered definitions.
|
||||
for id: StringName in [&"guest_elevator", &"guest_corridor"]:
|
||||
construction_minutes = maxi(construction_minutes, runtime.definition_registry.get_facility(id).build_minutes)
|
||||
for index: int in range(builds.size()):
|
||||
var payload := {&"definition_id": builds[index][0], &"origin": builds[index][1]}
|
||||
var result: CommandResult = runtime.submit_command(&"build_facility", payload,
|
||||
&"player", &"", 0, clock.absolute_minute, StringName("p23.build.%d" % index))
|
||||
ok = stream.record("build-%d" % index, {&"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok
|
||||
clock.advance_minutes(construction_minutes + 1)
|
||||
ok = stream.record("prepared", runtime.create_snapshot()) and ok
|
||||
runtime.domain_events.subscribe(&"*", &"p23.capture", Callable(self, "_event"))
|
||||
var view := WorldPreview.new()
|
||||
view.size = Vector2(1440, 810)
|
||||
view.set_runtime(runtime)
|
||||
tree.root.add_child(view)
|
||||
stream.watch(view)
|
||||
var finished: Dictionary = {}
|
||||
for offset: int in range(WINDOW + 1):
|
||||
if not ok: break
|
||||
if offset > 0: clock.advance_minutes(1)
|
||||
for arrival: Dictionary in input[&"arrivals"]:
|
||||
if arrival[&"offset"] != offset: continue
|
||||
var payload := {&"guest_id": arrival[&"id"], &"guest_type_id": &"traveler", &"gold": 100,
|
||||
&"options": {&"current_cell": START, &"state": GuestRuntime.State.MOVING,
|
||||
&"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000,
|
||||
&"path_handle": {&"target_cell": GOAL}}}
|
||||
var result: CommandResult = runtime.submit_command(&"spawn_guest", payload,
|
||||
&"world", arrival[&"id"], 10, -1, StringName("p23.spawn.%s" % arrival[&"id"]))
|
||||
ok = stream.record("arrival-%s" % arrival[&"id"], {&"offset": offset, &"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok
|
||||
# Typed per-minute projections retain paths, queues, density and actual
|
||||
# completed services. Whole aggregate snapshots are only sampled sparsely.
|
||||
var guest_records: Dictionary = runtime.guest_service.create_snapshot()
|
||||
ok = stream.record("tick-%03d" % offset, {&"minute": clock.absolute_minute,
|
||||
&"guests": guest_records, &"path": runtime.path_service.create_snapshot(),
|
||||
&"elevators": runtime.elevators.create_snapshot(), &"facilities": runtime.facility_service.create_snapshot()}) and ok
|
||||
for guest: Dictionary in guest_records[&"guests"]:
|
||||
if guest[&"current_cell"] != [GOAL.x, GOAL.y, GOAL.z] or finished.has(guest[&"guest_id"]): continue
|
||||
finished[guest[&"guest_id"]] = offset
|
||||
if offset % 30 == 0:
|
||||
var drawn: bool = await stream.await_draws(tree, view)
|
||||
ok = drawn and stream.capture("frame-%03d" % offset, runtime, view) and ok
|
||||
ok = stream.record("first-arrivals", {&"offsets": finished}) and ok
|
||||
ok = stream.record("final", runtime.create_snapshot()) and ok
|
||||
stream.close()
|
||||
view.free()
|
||||
runtime.free()
|
||||
clock.free()
|
||||
state.free()
|
||||
return ok
|
||||
|
||||
func _event(event: SimulationDomainEvent) -> void:
|
||||
stream.record("event", {&"type": event.event_type, &"minute": event.absolute_minute,
|
||||
&"entities": event.entity_ids, &"payload": event.payload.duplicate(true)})
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtxpm0n6cfom
|
||||
@@ -0,0 +1,164 @@
|
||||
extends "res://tests/fixtures/p08-03/placement.gd"
|
||||
## Real Runtime commands; inherited formal codec/save-operation helpers unchanged.
|
||||
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
|
||||
const Comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd")
|
||||
var stream: RefCounted
|
||||
var layout := "A"
|
||||
var facility_ids: Dictionary = {}
|
||||
|
||||
func setup() -> void:
|
||||
clock.reset(1, 6, 0)
|
||||
var state: Variant = tree.root.get_node("GameState")
|
||||
state.reset()
|
||||
expect(runtime.initialize(clock, state, 806042).get(&"valid", false), "initialize observation")
|
||||
clock.set_paused(true)
|
||||
tree.root.size = Vector2i(1440, 810)
|
||||
layout = OS.get_environment("P23_LAYOUT")
|
||||
expect(layout in ["A", "B"], "declared layout A or B")
|
||||
main = load("res://scenes/main/main.tscn").instantiate()
|
||||
main.manual_save_path = "user://saves/manual_ui"
|
||||
if role == "reader":
|
||||
import_package(directory.path_join("formal"), String(main.manual_save_path))
|
||||
tree.root.add_child(main)
|
||||
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
stream = Stream.new()
|
||||
expect(stream.open(directory.path_join(role + "-stream")), "fresh observation stream")
|
||||
stream.watch(main.world_preview)
|
||||
await frames()
|
||||
|
||||
func command(id: StringName, payload: Dictionary) -> bool:
|
||||
var result: CommandResult = runtime.submit_command(id, payload)
|
||||
expect(result.is_success(), String(id) + ": " + str(result.code))
|
||||
expect(stream.record("command", {&"command": id, &"payload": payload,
|
||||
&"result": result.to_dictionary(), &"minute": clock.absolute_minute}), "record public command")
|
||||
return result.is_success()
|
||||
|
||||
func advance(minutes: int) -> void:
|
||||
for unused: int in range(minutes):
|
||||
clock.advance_minutes(1)
|
||||
await frames()
|
||||
await observe_progress()
|
||||
if clock.absolute_minute % 30 == 0:
|
||||
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
|
||||
expect(stream.capture("minute-%d" % clock.absolute_minute, runtime, main.world_preview), "sparse minute observation")
|
||||
|
||||
func observe_progress() -> void:
|
||||
pass
|
||||
|
||||
func prepare() -> void:
|
||||
# Both layouts receive the same definition-derived diagnostic construction budget.
|
||||
# Cafeteria/kitchen move together to x=6, leaving x=4..5 for the declared B EV.
|
||||
var builds: Array = [[&"reception", Vector3i(-8, 0, 0)],
|
||||
[&"cafeteria", Vector3i(6, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)],
|
||||
[&"kitchen", Vector3i(6, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)],
|
||||
[&"standard_room", Vector3i(-8, 2, 0)], [&"guest_stairs", Vector3i(-4, 1, 0)],
|
||||
[&"guest_stairs", Vector3i(-4, 2, 0)], [&"guest_corridor", Vector3i(-5, 2, 0)],
|
||||
[&"guest_corridor", Vector3i(-6, 2, 0)], [&"guest_elevator", Vector3i.ZERO],
|
||||
[&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)],
|
||||
[&"staff_waiting_room", Vector3i(-4, 0, 1)]]
|
||||
var improvement: Dictionary = Comparison.new().declaration()[&"improvement"]
|
||||
var budget_builds := builds.duplicate(true)
|
||||
budget_builds.append([improvement[&"definition_id"], improvement[&"origin"]])
|
||||
var needed := {&"gold": 0, &"materials": 0}
|
||||
var costs: Array = []
|
||||
for row: Array in budget_builds:
|
||||
var definition: FacilityDefinition = runtime.definition_registry.get_facility(row[0])
|
||||
costs.append({&"definition": row[0], &"cost": definition.construction_cost.duplicate(true)})
|
||||
for key: StringName in needed:
|
||||
needed[key] += int(definition.construction_cost.get(key, 0))
|
||||
var state: Variant = tree.root.get_node("GameState")
|
||||
var deltas := {}
|
||||
for key: StringName in needed:
|
||||
if needed[key] > state.get_resource(key):
|
||||
deltas[key] = needed[key] - state.get_resource(key)
|
||||
budget = {"setup": "diagnostic", "costs": costs, "needed": needed,
|
||||
"initial": state.resources.duplicate(true), "deltas": deltas}
|
||||
if not deltas.is_empty(): command(&"change_resources", {&"deltas": deltas})
|
||||
expect(stream.record("budget", budget), "common diagnostic budget")
|
||||
if layout == "B": builds.append([improvement[&"definition_id"], improvement[&"origin"]])
|
||||
# Preserve the supply scenario's staged support/construction sequence.
|
||||
for index: int in range(builds.size()):
|
||||
if index == 5: await advance(180)
|
||||
if index == 7: await advance(60)
|
||||
if index == 8: await advance(10)
|
||||
command(&"build_facility", {&"definition_id": builds[index][0], &"origin": builds[index][1]})
|
||||
var transport_minutes := 0
|
||||
for id: StringName in [&"guest_elevator", &"guest_corridor"]:
|
||||
transport_minutes = maxi(transport_minutes, runtime.definition_registry.get_facility(id).build_minutes)
|
||||
await advance(maxi(10, transport_minutes))
|
||||
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
||||
facility_ids[facility.definition_id] = facility.instance_id
|
||||
command(&"set_facility_conditions", {&"facility_id": facility_ids[&"cafeteria"],
|
||||
&"conditions": {&"related_facility_id": facility_ids[&"kitchen"]}})
|
||||
for assignment: Array in [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"],
|
||||
[&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]:
|
||||
command(&"hire_staff_candidate", {&"candidate_id": assignment[0]})
|
||||
command(&"set_staff_work_assignment", {&"staff_id": assignment[0],
|
||||
&"facility_id": facility_ids[assignment[1]], &"template_id": &"DAY"})
|
||||
command(&"purchase_supplies", {&"warehouse_id": facility_ids[&"warehouse"],
|
||||
&"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
|
||||
command(&"set_kitchen_production", {&"facility_id": facility_ids[&"kitchen"],
|
||||
&"enabled": true, &"target_stock": 8})
|
||||
# The OS observation uses the already declared transport cohort, plus
|
||||
# ordinary public arrivals whose service choice remains Runtime-owned.
|
||||
# These commands are recorded and identical across A/B; no actor is moved,
|
||||
# despawned, or inserted into a room/queue by the fixture.
|
||||
var demand: Dictionary = Comparison.new().declaration()
|
||||
expect(stream.record("observation-demand", {"cohort": demand[&"arrivals"],
|
||||
"schedule": "all observation actors at the recorded current minute; offsets apply only to native layout comparison",
|
||||
"minute": clock.absolute_minute}), "declared observation demand")
|
||||
for arrival: Dictionary in demand[&"arrivals"]:
|
||||
command(&"spawn_guest", {&"guest_id": StringName("os-" + String(arrival[&"id"])),
|
||||
&"guest_type_id": &"traveler", &"gold": 100,
|
||||
&"options": {&"current_cell": Comparison.START, &"state": GuestRuntime.State.MOVING,
|
||||
&"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000,
|
||||
&"path_handle": {&"target_cell": Comparison.GOAL}}})
|
||||
command(&"spawn_guest", {&"guest_id": StringName("service-" + String(arrival[&"id"])),
|
||||
&"guest_type_id": &"traveler", &"gold": 100})
|
||||
await advance(120)
|
||||
|
||||
func layer_observations(prefix: String) -> void:
|
||||
await button(main.front_button)
|
||||
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
|
||||
expect(stream.capture(prefix + "-FRONT", runtime, main.world_preview), "FRONT state/report")
|
||||
await button(main.back_button)
|
||||
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
|
||||
expect(stream.capture(prefix + "-BACK", runtime, main.world_preview), "BACK state/report")
|
||||
|
||||
func writer() -> void:
|
||||
await prepare()
|
||||
await layer_observations("writer")
|
||||
capture("presave")
|
||||
await button(main.find_child("ManualSaveButton", true, false))
|
||||
expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save")
|
||||
capture("checkpoint")
|
||||
export_package(String(main.manual_save_path), "formal")
|
||||
var formal := inspect_formal()
|
||||
var presave := FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var"))
|
||||
expect(presave == var_to_bytes(formal), "whole presave formal model")
|
||||
expect(not operation_results.is_empty(), "save result recorded")
|
||||
if not operation_results.is_empty():
|
||||
verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED")
|
||||
var members := {}
|
||||
for member: String in MEMBERS:
|
||||
members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member))
|
||||
json_file("save.json", {"pid": OS.get_process_id(), "members": members})
|
||||
|
||||
func reader(distinct: bool) -> void:
|
||||
var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json")))
|
||||
expect(distinct and saved.pid != OS.get_process_id(), "distinct actual reader PID")
|
||||
await load_menu(main.find_child("SaveLoadMenu", true, false))
|
||||
await frames()
|
||||
expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume")
|
||||
capture("checkpoint")
|
||||
var formal := inspect_formal()
|
||||
expect(FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var")) == var_to_bytes(formal), "whole formal resume model")
|
||||
for member: String in MEMBERS:
|
||||
expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "formal member " + member)
|
||||
expect(not operation_results.is_empty(), "load result recorded")
|
||||
if not operation_results.is_empty():
|
||||
verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED")
|
||||
await layer_observations("reader")
|
||||
|
||||
func finish_observations() -> void:
|
||||
stream.close()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmoi8je5hse38
|
||||
@@ -0,0 +1,263 @@
|
||||
extends "res://tests/fixtures/p23-03/observation.gd"
|
||||
## Passive endpoint implementation. All gameplay/input is performed by Root CUA.
|
||||
## Shared parent contains the handler endpoints, but every such endpoint below
|
||||
## is overridden; this class never emits UI signals or invokes product handlers.
|
||||
var receipts: Array = []
|
||||
var terminated := false
|
||||
var terminal_record: Dictionary = {}
|
||||
var terminal_cause_recorded := false
|
||||
var reveal_rows: Array = []
|
||||
var all_requests: Array = []
|
||||
var input_observer: Node
|
||||
var last_dispatch: Dictionary = {}
|
||||
var observed_categories: Dictionary = {}
|
||||
|
||||
func observe_progress() -> void:
|
||||
if role != "writer" or terminated: return
|
||||
var wanted: Dictionary = {}
|
||||
var report: Dictionary = main.world_preview.get_life_observation_report()
|
||||
for room: Dictionary in report.get(&"rooms", {}).values():
|
||||
if int(room.get(&"count", 0)) > 0: wanted["room-users"] = true
|
||||
for badge: Dictionary in report.get(&"landing_badges", []):
|
||||
if int(badge.get(&"overflow", 0)) > 0: wanted["queue-overflow"] = true
|
||||
for cabin: Dictionary in report.get(&"cabins", []):
|
||||
if not cabin[&"passengers"].is_empty(): wanted["EV-passengers"] = true
|
||||
var elevators: Dictionary = runtime.elevators.get_report()
|
||||
for shaft: Dictionary in elevators.get(&"shafts", []):
|
||||
if not shaft.get(&"boarded", []).is_empty(): wanted["EV-passengers"] = true
|
||||
for trip: Dictionary in elevators.get(&"trips", []):
|
||||
if trip.get(&"state", &"") == &"QUEUED": wanted["queue-present"] = true
|
||||
for member: StaffRuntime in runtime.staff_service.get_staff_members():
|
||||
if not member.carried_inventory_id.is_empty(): wanted["carried-goods"] = true
|
||||
if not report.get(&"resting_staff", {}).is_empty(): wanted["rest-position"] = true
|
||||
for category: String in wanted:
|
||||
if observed_categories.has(category): continue
|
||||
observed_categories[category] = true
|
||||
await layer_observations("actual-" + category)
|
||||
if category == "EV-passengers": await motion_observations()
|
||||
|
||||
func motion_observations() -> void:
|
||||
await button(main.front_button)
|
||||
await button(main.pause_button)
|
||||
expect(not clock.paused, "ordinary resume for motion observation")
|
||||
var minute: int = clock.absolute_minute
|
||||
while clock.absolute_minute == minute and not terminated:
|
||||
await tree.process_frame
|
||||
await frames()
|
||||
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
|
||||
expect(stream.capture("active-motion", runtime, main.world_preview), "active interpolation history")
|
||||
await button(main.pause_button)
|
||||
expect(clock.paused, "ordinary pause for stable observation")
|
||||
await frames()
|
||||
expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture")
|
||||
expect(stream.capture("paused-motion", runtime, main.world_preview), "paused interpolation history")
|
||||
|
||||
class InputObserver extends Node:
|
||||
var events: Array = []
|
||||
var endpoint: RefCounted = null
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton or event is InputEventKey or event is InputEventMouseMotion:
|
||||
var row := {"ticks_usec": Time.get_ticks_usec(), "text": event.as_text(), "pressed": event.is_pressed()}
|
||||
if event is InputEventMouseButton:
|
||||
row["button"] = event.button_index
|
||||
row["position"] = [event.position.x, event.position.y]
|
||||
row["ctrl"] = event.ctrl_pressed
|
||||
if endpoint != null and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
row["dispatch"] = endpoint.dispatch_position(event.position)
|
||||
events.append(row)
|
||||
|
||||
func expect(ok: bool, message: String) -> void:
|
||||
if terminated and ok:
|
||||
return
|
||||
if terminated and terminal_cause_recorded:
|
||||
if not ok:
|
||||
terminal_record["suppressed_after_terminal"] += 1
|
||||
json_file(role + "-terminal.json", terminal_record)
|
||||
return
|
||||
if terminated and not ok:
|
||||
terminal_cause_recorded = true
|
||||
super.expect(ok, message)
|
||||
|
||||
func terminate(stem: String, reason: String) -> void:
|
||||
terminated = true
|
||||
terminal_record = {"pid": OS.get_process_id(), "role": role, "sequence": serial,
|
||||
"stem": stem, "reason": reason, "suppressed_after_terminal": 0}
|
||||
json_file(role + "-terminal.json", terminal_record)
|
||||
expect(false, reason)
|
||||
tree.quit(1)
|
||||
|
||||
func request(action: Dictionary) -> void:
|
||||
if terminated:
|
||||
return
|
||||
serial += 1
|
||||
var stem := "%s-request-%04d" % [role, serial]
|
||||
var folder := directory.path_join(stem)
|
||||
DirAccess.make_dir_recursive_absolute(folder)
|
||||
var window: Window = main.get_window()
|
||||
var view: WorldPreview = main.world_preview
|
||||
expect(await stream.await_draws(tree, view), "real draw history before capture")
|
||||
action["report"] = Codec.new().typed(view.get_life_observation_report())
|
||||
action["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse())
|
||||
action["local_to_viewport"] = affine(view.get_global_transform_with_canvas())
|
||||
action["local_to_screen"] = affine(view.get_screen_transform())
|
||||
action["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view))
|
||||
action["requested_ticks_usec"] = Time.get_ticks_usec()
|
||||
action["requested_unix_ns"] = int(Time.get_unix_time_from_system() * 1000000000.0)
|
||||
action["screenshot_receipt_schema"] = {"screenshots": [{"path": "evidence-root-relative raw image path", "raw_sha256": "original byte SHA256", "tool_call_id": "actual CUA screenshot call ID"}]}
|
||||
expect(stream.capture(stem, runtime, view), "request state/report")
|
||||
action.merge({"sequence": serial, "target_pid": OS.get_process_id(), "role": role,
|
||||
"state": "waiting_for_root_cua", "window_title": window.title,
|
||||
"window_position": [window.position.x, window.position.y],
|
||||
"window_size": [window.size.x, window.size.y], "screen_scale": DisplayServer.screen_get_scale(window.current_screen),
|
||||
"content_scale_factor": window.content_scale_factor, "setup": "diagnostic"})
|
||||
json_file(stem + "/request.json", action)
|
||||
var path := folder.path_join("ack.json")
|
||||
var first_event: int = input_observer.events.size()
|
||||
var deadline := Time.get_ticks_msec() + 300000
|
||||
print("P23_ROOT_CUA_WAIT " + folder.path_join("request.json"))
|
||||
while not FileAccess.file_exists(path) and Time.get_ticks_msec() < deadline:
|
||||
await tree.process_frame
|
||||
if not FileAccess.file_exists(path):
|
||||
terminate(stem, "Root CUA acknowledgement timeout")
|
||||
return
|
||||
var ack: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||
if not ack is Dictionary or ack.get("target_pid") != OS.get_process_id() or ack.get("sequence") != serial or ack.get("request_sha256") != FileAccess.get_sha256(folder.path_join("request.json")):
|
||||
terminate(stem, "Root CUA acknowledgement identity/hash")
|
||||
return
|
||||
await frames()
|
||||
for event: Dictionary in input_observer.events.slice(first_event):
|
||||
if event.has("dispatch"):
|
||||
last_dispatch = event["dispatch"]
|
||||
json_file(stem + "/observed.json", {"pid": OS.get_process_id(), "sequence": serial,
|
||||
"ack_observed_ticks_usec": Time.get_ticks_usec(), "request_sha256": FileAccess.get_sha256(folder.path_join("request.json")),
|
||||
"ack_observed_unix_ns": int(Time.get_unix_time_from_system() * 1000000000.0),
|
||||
"observer_endpoint": "null_modal_receipts_only" if input_observer.endpoint == null else "passive_read_only_dispatch",
|
||||
"events": input_observer.events.slice(first_event), "status": main.build_status_label.text})
|
||||
receipts.append("os-pair/" + layout + "/" + stem + "/ack.json")
|
||||
all_requests.append("os-pair/" + layout + "/" + stem + "/ack.json")
|
||||
|
||||
func annotate(row: Dictionary) -> void:
|
||||
var view: WorldPreview = main.world_preview
|
||||
row["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view))
|
||||
row["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse())
|
||||
row["reveal"] = reveal_rows.duplicate(true)
|
||||
reveal_rows.clear()
|
||||
row["input_method"] = "native_os"
|
||||
row["requests"] = receipts.duplicate()
|
||||
receipts.clear()
|
||||
|
||||
# Rectangles describe existing draw commands; no second renderer is invoked.
|
||||
func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary:
|
||||
var rows: Array = []
|
||||
for id: Variant in report.get(&"rooms", {}):
|
||||
var room: Dictionary = report[&"rooms"][id]
|
||||
if room.has(&"rect"):
|
||||
rows.append({"category": "room", "id": id, "rect": room[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"})
|
||||
for index: int in range(report.get(&"landing_badges", []).size()):
|
||||
var badge: Dictionary = report[&"landing_badges"][index]
|
||||
rows.append(badge_rectangle("queue", str(index), badge[&"at"], badge[&"text"]))
|
||||
for cabin: Dictionary in report.get(&"cabins", []):
|
||||
rows.append({"category": "EV", "id": cabin[&"shaft_id"], "rect": cabin[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"})
|
||||
for actor: Dictionary in cabin[&"passengers"]:
|
||||
rows.append(actor_rectangle(actor))
|
||||
for actor: Dictionary in report.get(&"actor_drawings", []):
|
||||
rows.append(actor_rectangle(actor))
|
||||
if actor[&"staff"] and report[&"resting_staff"].has(StringName(String(actor[&"key"]).trim_prefix("staff/"))):
|
||||
var rest := actor_rectangle(actor)
|
||||
rest["category"] = "rest"
|
||||
rows.append(rest)
|
||||
for index: int in range(report.get(&"badges", []).size()):
|
||||
var badge: Dictionary = report[&"badges"][index]
|
||||
rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"]))
|
||||
for entry: Dictionary in report.get(&"traffic", []):
|
||||
var traffic: Dictionary = entry[&"traffic"]
|
||||
if traffic[&"current_density"] > 0 or traffic[&"temporary_block"]:
|
||||
var cell: Vector3i = entry[&"cell"]
|
||||
rows.append({"category": "traffic", "id": str(cell.x) + "," + str(cell.y) + "," + str(cell.z),
|
||||
"rect": entry[&"rect"], "style_source": "scenes/main/world_preview.gd"})
|
||||
var transform := view.get_global_transform_with_canvas()
|
||||
for row: Dictionary in rows:
|
||||
row["viewport_rect"] = transform * (row["rect"] as Rect2)
|
||||
row["screen_rect"] = view.get_screen_transform() * (row["rect"] as Rect2)
|
||||
var source: String = FileAccess.get_file_as_string("res://" + row["style_source"])
|
||||
var regex := RegEx.new()
|
||||
regex.compile('Color\\("([0-9a-fA-F]{6})"')
|
||||
var colors: Array = []
|
||||
for found: RegExMatch in regex.search_all(source):
|
||||
if not colors.has(found.get_string(1)): colors.append(found.get_string(1))
|
||||
row["style_colors"] = colors
|
||||
return {"rows": rows}
|
||||
|
||||
func badge_rectangle(category: String, id: String, at: Vector2, text: String) -> Dictionary:
|
||||
var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x + 6
|
||||
return {"category": category, "id": id, "rect": Rect2(at, Vector2(width, 14)),
|
||||
"text": text, "font_width": width - 6, "style_source": "scenes/main/world_preview.gd"}
|
||||
|
||||
func actor_rectangle(actor: Dictionary) -> Dictionary:
|
||||
return {"category": "staff" if actor[&"staff"] else "guest", "id": actor[&"key"],
|
||||
"rect": Rect2(actor[&"at"], Vector2(16, 18) * float(actor[&"scale"])), "style_source": "scripts/ui/pixel_people.gd"}
|
||||
|
||||
func point(node: Control) -> Dictionary:
|
||||
var viewport: Vector2 = node.get_global_transform_with_canvas() * (node.size * 0.5)
|
||||
var screen: Vector2 = node.get_screen_transform() * (node.size * 0.5)
|
||||
return {"node": String(node.get_path()), "viewport": [viewport.x, viewport.y], "screen": [screen.x, screen.y], "window_id": node.get_window().get_window_id(),
|
||||
"viewport_to_screen": affine(node.get_screen_transform() * node.get_global_transform_with_canvas().affine_inverse())}
|
||||
|
||||
func setup() -> void:
|
||||
await super.setup()
|
||||
input_method = "native_os"
|
||||
expect(DisplayServer.get_name() != "headless", "real OS display")
|
||||
tree.root.title = "The Tree P23-03 " + layout + " " + role + " PID " + str(OS.get_process_id())
|
||||
input_observer = InputObserver.new()
|
||||
# No handler dispatch: this observer only records incoming OS events.
|
||||
tree.root.add_child(input_observer)
|
||||
json_file(role + "-profile.json", {"pid": OS.get_process_id(),
|
||||
"user_data_dir": OS.get_user_data_dir(), "engine": Engine.get_version_info(),
|
||||
"display": DisplayServer.get_name()})
|
||||
|
||||
func button(node: BaseButton) -> void:
|
||||
expect(node != null and not node.disabled, "reachable ordinary button")
|
||||
if node == null or node.disabled: return
|
||||
await request({"action": "click", "point": point(node), "button_text": str(node.get("text"))})
|
||||
|
||||
func load_menu(menu: MenuButton) -> void:
|
||||
var action := point(menu)
|
||||
action.merge({"action": "load_menu", "item_id": 0, "instructions": "Open this normal menu and choose manual load (item 0)."})
|
||||
await request(action)
|
||||
|
||||
func screenshot(label: String) -> void:
|
||||
await frames()
|
||||
await request({"action": "screenshot", "id": label, "layout": layout})
|
||||
|
||||
func layer_observations(prefix: String) -> void:
|
||||
if role == "writer" and prefix == "writer": await screenshot("calibration")
|
||||
await button(main.front_button)
|
||||
await screenshot(prefix + "-FRONT")
|
||||
await button(main.back_button)
|
||||
await screenshot(prefix + "-BACK")
|
||||
if role == "writer" and prefix == "writer": await ordinary_camera()
|
||||
|
||||
func ordinary_camera() -> void:
|
||||
var view: WorldPreview = main.world_preview
|
||||
var at := point(view)
|
||||
expect(not view.pan_mode, "ordinary zoom starts with move mode off")
|
||||
var zoom: float = view.zoom_level
|
||||
await request({"action": "wheel", "point": at, "direction": "down",
|
||||
"ctrl": false, "move_mode": false})
|
||||
expect(view.zoom_level != zoom, "ordinary wheel changes zoom")
|
||||
await screenshot("ordinary-wheel-zoom")
|
||||
await button(main.pan_button)
|
||||
await screenshot("pan-mode-button")
|
||||
expect(view.pan_mode, "PanModeButton enables move mode")
|
||||
zoom = view.zoom_level
|
||||
var pan: Vector2 = view.pan_offset
|
||||
await request({"action": "wheel", "point": point(view), "direction": "up",
|
||||
"ctrl": false, "move_mode": true})
|
||||
expect(view.pan_offset != pan and view.zoom_level == zoom, "ordinary move-mode wheel pans")
|
||||
await screenshot("move-mode-wheel-pan")
|
||||
|
||||
func finish_observations() -> void:
|
||||
json_file(role + "-requests.json", {"requests": all_requests, "pid": OS.get_process_id()})
|
||||
if is_instance_valid(input_observer): input_observer.queue_free()
|
||||
super.finish_observations()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://d3oqtdp6dodu1
|
||||
@@ -0,0 +1,8 @@
|
||||
extends SceneTree
|
||||
const Passive := preload("res://tests/fixtures/p23-03/passive_observation.gd")
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
func run() -> void:
|
||||
var result: Dictionary = await Passive.new().run(self, OS.get_environment("P23_OBSERVATION_ROOT"), OS.get_environment("P23_OBSERVATION_ROLE"))
|
||||
print("P23 passive OS observation: %d checks, %d failures" % [result.checks, result.failures])
|
||||
quit(0 if result.failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://mn33trympqu6
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"provenance": "Distilled layout-A frame-030 state ac3ed64a6e6162918e4d44c45b2f78d09e50d473b607f3469350a06074a7efb3; retain first real queued trip, no cabin seat. Unit fixture only.",
|
||||
"saved": {
|
||||
"version": 1,
|
||||
"current_minute": 691,
|
||||
"request_sequence": 8,
|
||||
"shafts": [{
|
||||
"config": {
|
||||
"shaft_id": "transport.shaft.0.0.0.2",
|
||||
"module_ids": ["facility_00000003"],
|
||||
"stops": [[0, 0, 0], [0, 1, 0]],
|
||||
"width": 2, "capacity": 4, "travel_minutes_per_floor": 10, "access_id": ""
|
||||
},
|
||||
"operational": true, "operator_open": true, "powered": true,
|
||||
"current_floor": 1, "target_floor": 1, "boarding_floor": 0,
|
||||
"phase": "PICKUP", "remaining_minutes": 9, "phase_total_minutes": 10,
|
||||
"phase_started_at": 690, "phase_origin_floor": 1, "phase_passenger_count": 0,
|
||||
"boarded": []
|
||||
}],
|
||||
"trips": [{
|
||||
"actor_id": "guest:p23-000", "request_id": "transport.trip.000000000002",
|
||||
"shaft_id": "transport.shaft.0.0.0.2",
|
||||
"connector_id": "transport.shaft.0.0.0.2.link.0.1",
|
||||
"from": [0, 0, 0], "to": [0, 1, 0], "enqueued_at": 674, "state": "QUEUED"
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
extends SceneTree
|
||||
## Component and read-only rendering checks; never OS or same-demand proof.
|
||||
const Presentation := preload("res://scripts/ui/room_presentation.gd")
|
||||
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func expect(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
push_error("P23 life observation: " + message)
|
||||
|
||||
func run() -> void:
|
||||
var layout := OS.get_environment("P23_LAYOUT")
|
||||
var output_directory := OS.get_environment("P23_OUTPUT")
|
||||
if layout in ["A", "B"] and not output_directory.is_empty():
|
||||
var comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd").new()
|
||||
expect(await comparison.run(self, output_directory, layout), "native layout " + layout)
|
||||
print("P23 life observation: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
return
|
||||
var clock: Variant = root.get_node("GameClock")
|
||||
clock.set_paused(true)
|
||||
# Isolated real typed components test state labels, without claiming gameplay.
|
||||
var labels: Dictionary = {}
|
||||
for state: int in FacilityInstance.State.values():
|
||||
var room := FacilityInstance.new().configure(&"component", &"reception")
|
||||
room.state = state as FacilityInstance.State
|
||||
var description := Presentation.describe(room)
|
||||
expect(description[&"state_name"] == FacilityInstance.State.keys()[state], "enum name preserved")
|
||||
var text := Presentation.business_label(description)
|
||||
expect(not text.is_empty() and not labels.has(text), "distinct visible business reason")
|
||||
labels[text] = true
|
||||
for reason: StringName in [&"RAIDED", &"DESTROYED"]:
|
||||
var room := FacilityInstance.new().configure(&"component", &"reception", {&"suspension_reason": reason})
|
||||
var description := Presentation.describe(room)
|
||||
expect(description[&"operation"] == FacilityInstance.OperationState.SUSPENDED, "actual suspension operation")
|
||||
var text := Presentation.business_label(description)
|
||||
expect(not labels.has(text), "raid and damage have separate visible labels")
|
||||
labels[text] = true
|
||||
var room := FacilityInstance.new().configure(&"component", &"reception", {&"customer_capacity": 3})
|
||||
var service := ServiceInstance.new()
|
||||
service.guest_ids.assign([&"b", &"a"])
|
||||
room.active_services[&"service"] = service
|
||||
room.queue_entries.assign([&"queue-only"])
|
||||
var description := Presentation.describe(room)
|
||||
expect(description[&"people"] == [&"a", &"b"] and description[&"count"] == 2, "queue entries are not room occupants")
|
||||
description[&"people"].clear()
|
||||
expect(service.guest_ids.size() == 2, "presentation owns its arrays")
|
||||
var runtime: Variant = root.get_node("SimulationRuntime")
|
||||
var view := WorldPreview.new()
|
||||
view.size = Vector2(1440, 810)
|
||||
view.set_runtime(runtime)
|
||||
root.add_child(view)
|
||||
var before: PackedByteArray = var_to_bytes(runtime.create_snapshot())
|
||||
var tracks: PackedByteArray = var_to_bytes([view._actor_tracks, view._elevator_tracks])
|
||||
var model := view._build_life_observation()
|
||||
expect(var_to_bytes(runtime.create_snapshot()) == before, "builder does not mutate Runtime")
|
||||
expect(var_to_bytes([view._actor_tracks, view._elevator_tracks]) == tracks, "builder does not advance visual tracks")
|
||||
expect(model.has(&"rooms") and model.has(&"landing_badges") and model.has(&"elevators") and model.has(&"staff") and model.has(&"goods") and model.has(&"badges"), "all six category fields exist")
|
||||
expect(model[&"traffic"].size() > 0 and not view.show_grid, "normal view observes real walkable traffic")
|
||||
await process_frame
|
||||
await process_frame
|
||||
var report := view.get_life_observation_report()
|
||||
expect(not report.is_empty(), "actual drawing published an observation")
|
||||
# The getter exposes the last actual draw and must not create another model.
|
||||
var original: PackedByteArray = var_to_bytes(report)
|
||||
if report.has(&"traffic"):
|
||||
report[&"traffic"].clear()
|
||||
expect(var_to_bytes(view.get_life_observation_report()) == original, "nested report arrays are isolated")
|
||||
report.clear()
|
||||
expect(var_to_bytes(view.get_life_observation_report()) == original, "public report is a deep copy")
|
||||
var output := OS.get_environment("P23_LIFE_DIAGNOSTIC_ROOT")
|
||||
if not output.is_empty():
|
||||
var stream := Stream.new()
|
||||
expect(stream.open(output), "fresh sparse diagnostic stream")
|
||||
expect(stream.capture("initial", runtime, view), "whole typed state and draw report capture")
|
||||
stream.close()
|
||||
view.free()
|
||||
print("P23 life observation: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d23lifeobs03
|
||||
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Root-only native collection. Does not run regression or claim acceptance."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import re
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
HERE = Path(__file__).resolve().parent
|
||||
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
|
||||
CONDITIONS = ('room_occupancy_and_operation', 'queues_and_overflow',
|
||||
'elevator_state_and_passengers', 'staff_state_and_jobs',
|
||||
'supplies_and_carried_goods', 'same_demand_route_improvement',
|
||||
'normal_os_and_distinct_pid_resume')
|
||||
OS_CHILD_TIMEOUT_SECONDS = 900
|
||||
RECORDER = 'tests/fixtures/p23-03/passive_os_recorder.gd'
|
||||
|
||||
def collect_screenshots(out, pair, role):
|
||||
"""Bind every external CUA screenshot to its request and decoded PNG.
|
||||
|
||||
The external receipt supplies screenshots[{path,raw_sha256,tool_call_id}].
|
||||
Paths are relative to the evidence root; raw receipt bytes remain unchanged.
|
||||
"""
|
||||
checker = module('p23_screenshot_binding', HERE/'check_p23_life_observation_evidence.py')
|
||||
bindings = []
|
||||
requests = json.loads((pair/(role+'-requests.json')).read_text())['requests']
|
||||
for ack_name in requests:
|
||||
ack_path = out/ack_name
|
||||
checker.require(ack_path.resolve().is_relative_to(out.resolve()), 'ACK path escape')
|
||||
ack = json.loads(ack_path.read_text())
|
||||
request_path = ack_path.with_name('request.json')
|
||||
request = json.loads(request_path.read_text())
|
||||
receipt_path = out/ack['input_receipt']
|
||||
checker.require(receipt_path.resolve().is_relative_to(out.resolve()), 'CUA receipt path escape')
|
||||
cua = json.loads(receipt_path.read_text())
|
||||
checker.require(ack['request_sha256'] == checker.sha(request_path), 'CUA request hash')
|
||||
checker.require(cua['screenshots'], 'CUA screenshots missing')
|
||||
for number, shot in enumerate(cua['screenshots']):
|
||||
raw = out/shot['path']
|
||||
checker.require(raw.resolve().is_relative_to(out.resolve()) and not raw.is_symlink(), 'screenshot path escape')
|
||||
checker.require(checker.sha(raw) == shot['raw_sha256'] and shot['tool_call_id'], 'raw screenshot CUA binding')
|
||||
kind, _, _ = checker.image_kind(raw.read_bytes())
|
||||
conversion = None
|
||||
if kind == 'jpeg':
|
||||
png = ack_path.with_name('decoded-%02d.png' % number)
|
||||
receipt = ack_path.with_name('conversion-%02d.json' % number)
|
||||
convert_screenshot(raw.resolve(), png.resolve(), shot, receipt.resolve())
|
||||
conversion = str(receipt.relative_to(out))
|
||||
else:
|
||||
png = raw
|
||||
width, height, _ = checker.png_pixels(png.read_bytes())
|
||||
bindings.append(dict(request=str(request_path.relative_to(out)), request_sha256=checker.sha(request_path),
|
||||
sequence=request['sequence'], target_pid=request['target_pid'],
|
||||
input_receipt=ack['input_receipt'], input_receipt_sha256=checker.sha(receipt_path),
|
||||
screenshot_index=number, tool_call_id=shot['tool_call_id'],
|
||||
raw_path=shot['path'], raw_sha256=shot['raw_sha256'], conversion=conversion,
|
||||
png_path=str(png.relative_to(out)), png_sha256=checker.sha(png), width=width, height=height))
|
||||
checker.require(bindings, 'empty screenshot collection')
|
||||
destination = pair/(role+'-screenshots.json')
|
||||
with destination.open('x') as output:
|
||||
json.dump(bindings, output, indent=2)
|
||||
output.write('\n')
|
||||
return str(destination.relative_to(out))
|
||||
|
||||
def convert_screenshot(raw_path, png_path, cua_receipt, receipt_path):
|
||||
"""Root-only conversion; raw CUA bytes are never renamed or rewritten."""
|
||||
checker = module('p23_conversion_checker', HERE/'check_p23_life_observation_evidence.py')
|
||||
raw, png, receipt = map(Path, (raw_path, png_path, receipt_path))
|
||||
checker.require(raw.is_absolute() and png.is_absolute() and receipt.is_absolute(),
|
||||
'absolute conversion paths required')
|
||||
checker.require(raw.is_file() and not raw.is_symlink() and not png.exists()
|
||||
and not receipt.exists() and raw.resolve() != png.resolve(), 'fresh separate conversion output')
|
||||
raw_hash = checker.sha(raw)
|
||||
checker.require(raw_hash == cua_receipt['raw_sha256'] and cua_receipt['tool_call_id'],
|
||||
'CUA conversion input hash/call mismatch')
|
||||
kind, width, height = checker.image_kind(raw.read_bytes())
|
||||
checker.require(kind == 'jpeg', 'conversion requires JPEG input')
|
||||
converter = '/usr/bin/sips'
|
||||
converter_hash = checker.sha(converter)
|
||||
argv = [converter, '-s', 'format', 'png', str(raw), '--out', str(png)]
|
||||
process = subprocess.run(argv, capture_output=True, timeout=900, check=False)
|
||||
rec = dict(raw_path=str(raw), raw_sha256=raw_hash, raw_kind=kind,
|
||||
tool_call_id=cua_receipt['tool_call_id'], converter=converter,
|
||||
converter_sha256=converter_hash, argv=argv, exit_code=process.returncode,
|
||||
stdout_sha256=hashlib.sha256(process.stdout).hexdigest(),
|
||||
stderr_sha256=hashlib.sha256(process.stderr).hexdigest(),
|
||||
png_path=str(png), png_sha256=checker.sha(png) if png.is_file() else None,
|
||||
width=width, height=height)
|
||||
# Preserve failure receipts too. These logs allow independent hash checking.
|
||||
with receipt.open('x') as output:
|
||||
json.dump(rec, output, indent=2)
|
||||
output.write('\n')
|
||||
with Path(str(receipt) + '.stdout').open('xb') as output:
|
||||
output.write(process.stdout)
|
||||
with Path(str(receipt) + '.stderr').open('xb') as output:
|
||||
output.write(process.stderr)
|
||||
checker.require(checker.sha(raw) == raw_hash, 'raw CUA bytes changed during conversion')
|
||||
checker.require(checker.sha(converter) == converter_hash, 'converter changed during conversion')
|
||||
checker.inspect_conversion(rec, cua_receipt, converter_hash)
|
||||
return rec
|
||||
|
||||
def collect_os(r, root, out, stage, engine, commit, refs, protected, outer):
|
||||
"""Adapted P08-03 collect_os protocol, retaining owned-process guards.
|
||||
|
||||
Root supplies CUA ack/receipt files in the requested directories. Neither
|
||||
controller nor passive endpoints generate input or change permissions.
|
||||
"""
|
||||
r.require(not outer.native.helper['live_engines'](), 'external engine present')
|
||||
checker = module('p23_os_pins', HERE/'check_p23_life_observation_evidence.py')
|
||||
before = r.inventory(stage, stage=True)
|
||||
public = (stage/'project.godot').read_bytes()
|
||||
(out/'os-pair').mkdir()
|
||||
phases = []
|
||||
for label in ('A-writer', 'A-reader', 'B-writer', 'B-reader'):
|
||||
layout, role = label.split('-')
|
||||
pair = out/'os-pair'/layout
|
||||
pair.mkdir(exist_ok=role == 'reader')
|
||||
r.require(not outer.native.helper['live_engines'](), 'external engine present')
|
||||
r.require(r.source_refs(root) == refs and r.default_player(root) == protected, 'OS before guards')
|
||||
r.require(r.inventory(stage, stage=True) == before, 'OS stage before guard')
|
||||
r.require(shutil.disk_usage(out).free >= 500*1024*1024, 'OS disk admission')
|
||||
folder = out/'os-runs'/label
|
||||
folder.mkdir(parents=True)
|
||||
name = 'TheTree-PCK-tests-'+uuid.uuid4().hex+'-p23-observation-'+label.lower()
|
||||
profile = r.profile_path(name)
|
||||
r.require(not profile.exists(), 'fresh OS private profile')
|
||||
formal_source = pair/'formal' if role == 'reader' else None
|
||||
formal_before = r.inventory(formal_source) if formal_source else {}
|
||||
if role == 'reader':
|
||||
r.require(set(formal_before) == {'world.bin','manifest.json','thumbnail.png','journal.log'}, 'all formal inputs')
|
||||
formal = profile/'saves/manual_ui'
|
||||
formal.mkdir(parents=True)
|
||||
for member, pin in formal_before.items():
|
||||
r.copy_source(formal_source/member, formal/member)
|
||||
r.require(r.sha(formal/member) == pin['sha256'], 'formal fixture copy SHA')
|
||||
recorder = folder/'passive_os.gd'
|
||||
r.copy_source(root/RECORDER, recorder)
|
||||
proof = folder/'teardown'
|
||||
(proof/'owned-audio').mkdir(parents=True)
|
||||
env = r.fresh_environment(dict(os.environ))
|
||||
env.update(P23_OBSERVATION_ROOT=str(pair), P23_OBSERVATION_ROLE=role, P23_LAYOUT=layout,
|
||||
THE_TREE_AUDIO_TEARDOWN_PROOF_DIR=str(proof/'owned-audio'),
|
||||
THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF=str(proof/'control.json'))
|
||||
command = [engine, '--path', str(stage), '--script', str(recorder)]
|
||||
base = {'protocol':'p23-observation-native-os-v1', 'role':role, 'layout':layout, 'commit':commit,
|
||||
'source_map':{n:v['sha256'] for n,v in refs.items()}, 'stage':str(stage),
|
||||
'stage_before':before, 'profile':str(profile), 'profile_name':name,
|
||||
'profile_before':{'existed':profile.exists(), 'files':r.inventory(profile) if profile.exists() else {}}, 'formal_input':formal_before,
|
||||
'formal_source':str(formal_source) if formal_source else None, 'owner_pid':os.getpid(),
|
||||
'default_before':protected, 'command':command, 'engine_sha256':r.sha(engine),
|
||||
'controller_sha256':r.sha(root/'tools/run_p23_life_observation.py'),
|
||||
'recorder_sha256':r.sha(recorder), 'formal_acceptance':False,
|
||||
'timeout_seconds':OS_CHILD_TIMEOUT_SECONDS,
|
||||
'inputs':checker.input_map(root), 'spec_sha256':checker.SPEC_SHA,
|
||||
'settings_sha256':hashlib.sha256(public).hexdigest(),
|
||||
'environment':{k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))},
|
||||
'environment_sha256':hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest()}
|
||||
r.write(folder/'started.json', base)
|
||||
error = None; children = []; guards = []; exit_code = None; owner_proof = None
|
||||
try:
|
||||
(stage/'project.godot').write_bytes(r.private_project(public, name))
|
||||
with (folder/'engine-native.log').open('x') as log:
|
||||
process = subprocess.Popen(command, cwd=stage, env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
children.append(process)
|
||||
r.write(folder/'process-started.json', dict(base, actual_pid=process.pid,
|
||||
monotonic_ns=time.monotonic_ns(), state='waiting_for_root_cua_requests'))
|
||||
print(json.dumps({'actual_pid':process.pid, 'request_directory':str(pair),
|
||||
'state':'waiting_for_root_cua', 'role':role}), flush=True)
|
||||
exit_code = process.wait(timeout=base['timeout_seconds'])
|
||||
r.require(exit_code == 0, 'ordinary native OS exit0')
|
||||
raw = (folder/'engine-native.log').read_text()
|
||||
r.require('ERROR:' not in raw and 'SCRIPT ERROR:' not in raw, 'OS native diagnostics')
|
||||
matches = re.findall(r'(?m)^P23 passive OS observation: ([0-9]+) checks, 0 failures$', raw)
|
||||
r.require(len(matches) == 1 and int(matches[0]) > 0, 'passive observer completion')
|
||||
observed = r.read(pair/(role+'-profile.json'))
|
||||
r.require(observed['pid'] == process.pid and Path(observed['user_data_dir']).resolve() == profile.resolve()
|
||||
and observed['display'] != 'headless', 'actual OS private profile/display')
|
||||
collect_screenshots(out, pair, role)
|
||||
owners = r.module('p23_os_owner_gate', r.SUPPORT/'ordinary_proof_gate.py')
|
||||
owner_proof = owners.ordinary_gate(proof, process.pid, folder, ['single'])
|
||||
except BaseException as caught:
|
||||
error = type(caught).__name__+': '+str(caught)
|
||||
finally:
|
||||
try:
|
||||
cleanup, cleanup_errors = outer.finish_captured(children)
|
||||
finally:
|
||||
(stage/'project.godot').write_bytes(public)
|
||||
try:
|
||||
r.require(r.source_refs(root) == refs, 'OS source unchanged')
|
||||
r.require(r.inventory(stage, stage=True) == before, 'OS stage unchanged')
|
||||
r.require(r.default_player(root) == protected, 'OS default player unchanged')
|
||||
if formal_source:
|
||||
r.require(r.inventory(formal_source) == formal_before, 'formal input unchanged')
|
||||
r.require(r.sha(recorder) == base['recorder_sha256'], 'recorder unchanged')
|
||||
except BaseException as caught:
|
||||
guards.append(type(caught).__name__+': '+str(caught))
|
||||
passed = not (error or guards or cleanup_errors) and len(children) == 1 and exit_code == 0
|
||||
r.write(folder/'ended.json', dict(base, actual_pid=children[0].pid if children else None,
|
||||
exit_code=exit_code, engine_ended=bool(cleanup) and all(v['engine_ended'] for v in cleanup),
|
||||
error=error, guard_errors=guards, cleanup_errors=cleanup_errors, owned_cleanup=cleanup,
|
||||
ordinary_owner_proof=owner_proof, stage_after=r.inventory(stage,stage=True),
|
||||
engine_sha256_after=r.sha(engine), inputs_after=checker.input_map(root),
|
||||
settings_sha256_after=hashlib.sha256((stage/'project.godot').read_bytes()).hexdigest(),
|
||||
environment_after={k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))},
|
||||
environment_sha256_after=hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest(),
|
||||
default_after=r.default_player(root), source_after=r.source_refs(root),
|
||||
profile_after=r.inventory(profile), passed=passed))
|
||||
phases.append('os-runs/'+label+'/ended.json')
|
||||
r.require(passed, 'native OS collection failed: '+str(error or guards or cleanup_errors))
|
||||
return phases
|
||||
|
||||
def module(name, path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
obj = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(obj)
|
||||
return obj
|
||||
|
||||
def absolute(value):
|
||||
path = Path(value)
|
||||
if not path.is_absolute():
|
||||
raise argparse.ArgumentTypeError('absolute path required')
|
||||
return path.resolve()
|
||||
|
||||
def collect(args):
|
||||
root, out = args.project_root, args.output
|
||||
if out.is_relative_to(root):
|
||||
raise ValueError('evidence output must be outside the frozen source checkout')
|
||||
r = module('p23_standard_runner', root/'tools/run_all_tests.py')
|
||||
commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
|
||||
r.require(commit == args.commit, 'actual final submitted commit required')
|
||||
r.require(not subprocess.check_output(['git','-C',str(root),'status','--porcelain'], text=True), 'frozen clean worktree required')
|
||||
r.require(not out.exists(), 'fresh output; never overwrite earlier proof')
|
||||
refs = r.source_refs(root)
|
||||
checker = module('p23_initial_inputs', root/'tools/check_p23_life_observation_evidence.py')
|
||||
inputs_before = checker.input_map(root)
|
||||
outer = r.module('p23_owned_outer', r.SUPPORT/'owned_outer.py')
|
||||
r.require(not outer.native.helper['live_engines'](), 'one native engine')
|
||||
engine = shutil.which(args.godot)
|
||||
r.require(engine is not None, 'native engine exists')
|
||||
engine = str(Path(engine).resolve())
|
||||
out.mkdir(parents=True)
|
||||
stage = out/'stage'; stage.mkdir()
|
||||
(out/'starters').mkdir(); (out/'bootstrap').mkdir()
|
||||
for name, pin in r.read(r.SUPPORT/'bootstrap-scripts.json').items():
|
||||
data = pin['text'].encode()
|
||||
r.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'bootstrap pin')
|
||||
(out/'bootstrap'/name).write_bytes(data)
|
||||
for name, pin in refs.items():
|
||||
target = stage/name; target.parent.mkdir(parents=True, exist_ok=True)
|
||||
r.copy_source(Path(pin['path']), target)
|
||||
r.require(r.sha(target) == pin['sha256'], 'whole copied source '+name)
|
||||
expected = {n: {'bytes':v['bytes'], 'sha256':v['sha256']} for n,v in refs.items()}
|
||||
selected = {'root_before':refs, 'map':{n:v['sha256'] for n,v in refs.items()}, 'timeout':180}
|
||||
r.require(r.inventory(stage, stage=True) == expected, 'complete private source')
|
||||
protected = r.default_player(root); public = (stage/'project.godot').read_bytes()
|
||||
paths = ['res://'+n for n in sorted(refs) if n.endswith('.gd')]
|
||||
r.write(out/'parse-paths.json', paths)
|
||||
markers = r.module('p23_strict_markers', root/'tools/strict_test_markers.py')
|
||||
contracts = markers.load_contracts(root/CONTRACT)
|
||||
policy = r.read(r.SUPPORT/'suite-policy.json')
|
||||
diagnostics = r.module('p23_diagnostics', r.SUPPORT/'diagnostic_gate.py')
|
||||
owners = r.module('p23_owners', r.SUPPORT/'ordinary_proof_gate.py')
|
||||
audio = r.module('p23_audio', r.SUPPORT/'audio_evidence_gate.py')
|
||||
r.write(out/'runner-input.json', {'protocol':'p23-normal-observation-v1', 'commit':commit,
|
||||
'selected_source_map':selected['map'], 'engine':engine, 'engine_sha256':r.sha(engine),
|
||||
'owner_pid':os.getpid(), 'default_player':protected, 'diagnostic_seed':806042,
|
||||
'inputs':inputs_before, 'provenance':checker.input_provenance(root),
|
||||
'pixel_decoder':{'interpreter':sys.executable,
|
||||
'PIL_available':importlib.util.find_spec('PIL') is not None,
|
||||
'raw_screenshots':'preserve bytes; Root lossless conversion required for JPEG without PIL'}})
|
||||
phases = []; previous = {}; cases = []; error = None; os_proof = None
|
||||
deadline = time.monotonic()+900
|
||||
saved_env = {k:os.environ.get(k) for k in ('P23_LAYOUT','P23_OUTPUT')}
|
||||
def phase(label, test):
|
||||
remaining = int(deadline-time.monotonic())
|
||||
r.require(remaining >= (180 if label == 'import' else 1), '900s collection deadline')
|
||||
selected['timeout'] = min(180, remaining)
|
||||
phase_out = out/'runs'/label
|
||||
phase_out.mkdir(parents=True)
|
||||
(phase_out/'starters').mkdir(); (phase_out/'bootstrap').mkdir()
|
||||
for original in (out/'bootstrap').iterdir():
|
||||
r.copy_source(original,phase_out/'bootstrap'/original.name)
|
||||
r.require(r.sha(original)==r.sha(phase_out/'bootstrap'/original.name),'bootstrap full copy')
|
||||
r.write(phase_out/'parse-paths.json',paths)
|
||||
row = r.execute_phase(engine, stage, phase_out, label, test, selected, expected, previous,
|
||||
policy, contracts, paths, public, protected, root, outer, markers, diagnostics, owners, audio)
|
||||
# Supplement the existing runner receipt; preserve it verbatim.
|
||||
r.write(phase_out/'p23-inputs.json', dict(source_map=selected['map'], source_after=r.source_refs(root),
|
||||
engine_sha256=r.sha(engine), inputs=inputs_before, inputs_after=checker.input_map(root),
|
||||
spec_sha256=checker.SPEC_SHA, settings_sha256=hashlib.sha256(public).hexdigest(),
|
||||
stage_before=expected, stage_after=r.inventory(stage,stage=True),
|
||||
default_before=protected, default_after=r.default_player(root),
|
||||
receipt_sha256=r.sha(phase_out/(label+'-receipt.json'))))
|
||||
phases.append('runs/'+label+'/'+label+'-receipt.json')
|
||||
r.require(not row['failed'], 'strict phase failed: '+label)
|
||||
try:
|
||||
phase('import', None); phase('parse', None)
|
||||
for layout in ('A', 'B'):
|
||||
directory = out/('layout-'+layout)
|
||||
os.environ.update(P23_LAYOUT=layout, P23_OUTPUT=str(directory))
|
||||
phase('layout-'+layout.lower(), 'tests/ui/test_p23_life_observation.gd')
|
||||
cases.append({'directory':directory.name, 'phase':phases[-1], 'files':r.inventory(directory)})
|
||||
if args.input_mode == 'native_os':
|
||||
os_phases = collect_os(r, root, out, stage, engine, commit, refs, protected, outer)
|
||||
checker = module('p23_os_input_map', root/'tools/check_p23_life_observation_evidence.py')
|
||||
os_proof = 'os-proof.json'
|
||||
r.write(out/os_proof, {'source_map':selected['map'], 'inputs':checker.input_map(root),
|
||||
'input_method':'native_os', 'observer_mutations':[], 'phases':os_phases, 'directory':'os-pair'})
|
||||
except BaseException as caught:
|
||||
error = type(caught).__name__+': '+str(caught)
|
||||
finally:
|
||||
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
|
||||
for key,value in saved_env.items():
|
||||
if value is None: os.environ.pop(key, None)
|
||||
else: os.environ[key] = value
|
||||
if r.source_refs(root) != refs or r.default_player(root) != protected:
|
||||
error = (error+'; ' if error else '')+'protected root/profile changed'
|
||||
if checker.input_map(root) != inputs_before:
|
||||
error = (error+'; ' if error else '')+'spec/fixture/controller inputs changed'
|
||||
r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors,
|
||||
'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'})
|
||||
checker = module('p23_observation_checker', root/'tools/check_p23_life_observation_evidence.py')
|
||||
r.write(out/'index.json', {'schema':'p23-life-observation-v1', 'commit':commit,
|
||||
'source_map':selected['map'], 'input_map':inputs_before,
|
||||
'provenance':checker.input_provenance(root),
|
||||
'marker_contract_sha256':r.sha(root/CONTRACT),
|
||||
'spec_sha256':hashlib.sha256((root/'docs/spec/acceptance/P23-03.json').read_text().strip().encode()).hexdigest(),
|
||||
'cases':cases, 'phases':phases, 'conditions':list(CONDITIONS),
|
||||
'artifacts':{str(p.relative_to(out)):r.sha(p)
|
||||
for p in out.rglob('*') if p.is_file() and 'stage' not in p.relative_to(out).parts},
|
||||
'os_proof':os_proof, 'regression':None,
|
||||
'acceptance':'pending', 'collection_error':error})
|
||||
return 1 if error or cleanup_errors else 0
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--project-root', type=absolute, required=True)
|
||||
parser.add_argument('--output', type=absolute, required=True)
|
||||
parser.add_argument('--commit', required=True)
|
||||
parser.add_argument('--godot', default='godot')
|
||||
parser.add_argument('--input-mode', choices=('handler', 'native_os'), default='handler')
|
||||
parser.add_argument('--collect', action='store_true', required=True)
|
||||
return collect(parser.parse_args(argv))
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,761 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local semantic-checker tests; synthetic examples are never game evidence."""
|
||||
import copy
|
||||
import ast
|
||||
import unittest
|
||||
import sys
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import hashlib
|
||||
import struct
|
||||
import zlib
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
import check_p23_life_observation_evidence as check
|
||||
|
||||
|
||||
class QueuedElevatorTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/queued_elevator.json').read_text())
|
||||
self.state = {'systems': {'runtime': {'elevators': fixture['saved']}}}
|
||||
|
||||
def test_queued_trip_without_cabin_seat_uses_config_shaft(self):
|
||||
saved = self.state['systems']['runtime']['elevators']
|
||||
self.assertNotIn('shaft_id', saved['shafts'][0])
|
||||
self.assertEqual(saved['shafts'][0]['boarded'], [])
|
||||
report = check.elevator_report(self.state)
|
||||
self.assertEqual(report['shafts'][0]['waiting_count'], 1)
|
||||
self.assertEqual(report['shafts'][0]['passenger_count'], 0)
|
||||
self.assertAlmostEqual(report['shafts'][0]['visual_floor'], .9)
|
||||
self.assertEqual(report['trips'], saved['trips'])
|
||||
|
||||
def test_tampered_shaft_attribution_fails(self):
|
||||
self.state['systems']['runtime']['elevators']['trips'][0]['shaft_id'] = 'wrong.shaft'
|
||||
with self.assertRaisesRegex(ValueError, 'shaft attribution'):
|
||||
check.elevator_report(self.state)
|
||||
|
||||
def test_wrong_waiting_count_fails_whole_report_comparison(self):
|
||||
expected = check.elevator_report(self.state)
|
||||
tampered = copy.deepcopy(expected)
|
||||
tampered['shafts'][0]['waiting_count'] = 0
|
||||
directory = mock.MagicMock()
|
||||
manifest = dict(label='queued-fixture', pid=123, state='state',
|
||||
state_after='state', report='report')
|
||||
directory.__truediv__.return_value.open.return_value = io.StringIO(json.dumps(manifest)+'\n')
|
||||
# Exercise the actual stream comparison; isolate unrelated actor fields.
|
||||
with mock.patch.object(check, 'read_object', side_effect=[self.state, self.state,
|
||||
{'elevators': tampered}]), \
|
||||
mock.patch.object(check, 'compare_actor_rows', return_value=[]):
|
||||
with self.assertRaisesRegex(ValueError, 'typed elevator report mismatch'):
|
||||
check.inspect_stream(directory)
|
||||
|
||||
def test_extra_and_missing_trip_keys_fail(self):
|
||||
for mutation in ('extra', 'missing'):
|
||||
with self.subTest(mutation=mutation):
|
||||
state = copy.deepcopy(self.state)
|
||||
trip = state['systems']['runtime']['elevators']['trips'][0]
|
||||
if mutation == 'extra':
|
||||
trip['unexpected'] = True
|
||||
else:
|
||||
del trip['shaft_id']
|
||||
with self.assertRaisesRegex(ValueError, 'exact elevator trip keys'):
|
||||
check.elevator_report(state)
|
||||
|
||||
|
||||
class SnapshotCellTests(unittest.TestCase):
|
||||
def test_layout_arrival_compares_serialized_cell_arrays(self):
|
||||
source = (check.ROOT / 'tests/fixtures/p23-03/layout_comparison.gd').read_text()
|
||||
serializer = (check.ROOT / 'scripts/guest/guest_runtime.gd').read_text()
|
||||
self.assertIn('&"current_cell": _cell_to_array(current_cell)', serializer)
|
||||
goal = check.re.search(r'const GOAL := Vector3i\(([^)]+)\)', source)
|
||||
self.assertIsNotNone(goal)
|
||||
coordinates = ast.literal_eval('[' + goal.group(1) + ']')
|
||||
comparison = check.re.search(r'if guest\[&"current_cell"\] != (.+?) or finished\.has', source)
|
||||
self.assertIsNotNone(comparison)
|
||||
expression = comparison.group(1)
|
||||
self.assertEqual(expression, '[GOAL.x, GOAL.y, GOAL.z]')
|
||||
for axis, value in zip('xyz', coordinates):
|
||||
expression = expression.replace('GOAL.' + axis, str(value))
|
||||
target = ast.literal_eval(expression)
|
||||
self.assertEqual(json.loads('[2, 1, 0]'), target)
|
||||
for cell in ([2, 0, 0], [3, 1, 0], [2, 1, 1]):
|
||||
self.assertNotEqual(cell, target)
|
||||
|
||||
|
||||
class ImageTests(unittest.TestCase):
|
||||
def test_collector_conversion_is_bound_to_each_cua_screenshot(self):
|
||||
runner = check.module('p23_unit_collector', check.ROOT/'tools/run_p23_life_observation.py')
|
||||
with tempfile.TemporaryDirectory(dir=check.ROOT/'tests/fixtures/p23-03') as temporary:
|
||||
root = Path(temporary)
|
||||
pair = root/'os-pair/A'
|
||||
request_dir = pair/'writer-request-0001'
|
||||
request_dir.mkdir(parents=True)
|
||||
request = request_dir/'request.json'
|
||||
request.write_text(json.dumps(dict(sequence=1, target_pid=123)))
|
||||
raw = request_dir/'raw.jpg'
|
||||
raw.write_bytes(b'\xff\xd8unit-only\xff\xd9')
|
||||
shot = dict(path=str(raw.relative_to(root)), raw_sha256=check.sha(raw), tool_call_id='unit-shot-call')
|
||||
receipt = request_dir/'cua.json'
|
||||
receipt.write_text(json.dumps(dict(screenshots=[shot])))
|
||||
ack = request_dir/'ack.json'
|
||||
ack.write_text(json.dumps(dict(request_sha256=check.sha(request), input_receipt=str(receipt.relative_to(root)))))
|
||||
(pair/'writer-requests.json').write_text(json.dumps(dict(requests=[str(ack.relative_to(root))])))
|
||||
def convert(raw_path, png_path, cua, receipt_path):
|
||||
self.assertEqual(cua, shot)
|
||||
self.assertEqual(Path(raw_path).read_bytes(), b'\xff\xd8unit-only\xff\xd9')
|
||||
Path(png_path).write_bytes(self.png())
|
||||
Path(receipt_path).write_text('{}')
|
||||
with mock.patch.object(runner, 'module', return_value=check), \
|
||||
mock.patch.object(check, 'image_kind', return_value=('jpeg', 2, 2)), \
|
||||
mock.patch.object(runner, 'convert_screenshot', side_effect=convert) as converter:
|
||||
output = runner.collect_screenshots(root, pair, 'writer')
|
||||
converter.assert_called_once()
|
||||
rows = json.loads((root/output).read_text())
|
||||
self.assertEqual(rows[0]['tool_call_id'], shot['tool_call_id'])
|
||||
self.assertEqual(rows[0]['raw_sha256'], check.sha(raw))
|
||||
self.assertEqual(rows[0]['png_sha256'], check.sha(root/rows[0]['png_path']))
|
||||
self.assertTrue(rows[0]['conversion'])
|
||||
|
||||
@staticmethod
|
||||
def png(mode=0, channels=3):
|
||||
def chunk(kind, body):
|
||||
return struct.pack('>I', len(body)) + kind + body + struct.pack('>I', zlib.crc32(kind + body))
|
||||
rows = [bytes([10, 20, 30, 40][:channels] * 2),
|
||||
bytes([50, 60, 70, 80][:channels] * 2)]
|
||||
filtered = bytearray()
|
||||
prior = bytes(2 * channels)
|
||||
for row in rows:
|
||||
filtered.append(mode)
|
||||
for x, value in enumerate(row):
|
||||
left, up, corner = (row[x-channels] if x >= channels else 0,
|
||||
prior[x], prior[x-channels] if x >= channels else 0)
|
||||
p = left + up - corner
|
||||
distances = [abs(p-left), abs(p-up), abs(p-corner)]
|
||||
predictors = [0, left, up, (left+up)//2,
|
||||
[left, up, corner][distances.index(min(distances))]]
|
||||
filtered.append((value-predictors[mode]) % 256)
|
||||
prior = row
|
||||
header = struct.pack('>IIBBBBB', 2, 2, 8, 2 if channels == 3 else 6, 0, 0, 0)
|
||||
return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', header)
|
||||
+ chunk(b'IDAT', zlib.compress(filtered)) + chunk(b'IEND', b''))
|
||||
|
||||
def test_all_png_filters_rgb_and_rgba(self):
|
||||
for mode in range(5):
|
||||
for channels in (3, 4):
|
||||
with self.subTest(mode=mode, channels=channels):
|
||||
w, h, rows = check.png_pixels(self.png(mode, channels))
|
||||
self.assertEqual((w, h), (2, 2))
|
||||
self.assertEqual(rows[0][0], tuple([10, 20, 30, 40][:channels]))
|
||||
self.assertEqual(rows[1][1], tuple([50, 60, 70, 80][:channels]))
|
||||
|
||||
def test_crc_and_trailing_bytes_rejected(self):
|
||||
data = bytearray(self.png())
|
||||
data[29] ^= 1
|
||||
with self.assertRaisesRegex(ValueError, 'CRC mismatch'):
|
||||
check.png_pixels(bytes(data))
|
||||
with self.assertRaisesRegex(ValueError, 'invalid IEND'):
|
||||
check.png_pixels(self.png() + b'extra')
|
||||
|
||||
def conversion(self):
|
||||
raw, png = b'\xff\xd8synthetic-unit-test\xff\xd9', self.png()
|
||||
digest = lambda b: hashlib.sha256(b).hexdigest()
|
||||
rec = dict(raw_path='/raw.png', raw_sha256=digest(raw), raw_kind='jpeg',
|
||||
tool_call_id='unit-test-call', converter='/usr/bin/sips', converter_sha256='a'*64,
|
||||
argv=['/usr/bin/sips', '-s', 'format', 'png', '/raw.png', '--out', '/decoded.png'],
|
||||
exit_code=0, stdout_sha256=digest(b''), stderr_sha256=digest(b''),
|
||||
png_path='/decoded.png', png_sha256=digest(png), width=2, height=2)
|
||||
cua = dict(raw_sha256=digest(raw), tool_call_id='unit-test-call')
|
||||
return rec, cua, {'/raw.png': raw, '/decoded.png': png}
|
||||
|
||||
def test_conversion_chain_and_specific_forgery_deficits(self):
|
||||
rec, cua, files = self.conversion()
|
||||
helper = mock.Mock()
|
||||
helper.jpeg_dimensions.return_value = (2, 2)
|
||||
with mock.patch.object(Path, 'read_bytes', lambda p: files[str(p)]), \
|
||||
mock.patch.object(check, 'module', return_value=helper):
|
||||
self.assertEqual(check.inspect_conversion(rec, cua, 'a'*64)[:2], (2, 2))
|
||||
for key, value, deficit in [('converter_sha256', 'b'*64, 'converter SHA mismatch'),
|
||||
('argv', [], 'converter argv mismatch'),
|
||||
('raw_sha256', 'b'*64, 'CUA raw hash/call mismatch'),
|
||||
('width', 3, 'conversion dimensions mismatch')]:
|
||||
with self.subTest(key=key):
|
||||
bad = dict(rec, **{key: value})
|
||||
with self.assertRaisesRegex(ValueError, deficit):
|
||||
check.inspect_conversion(bad, cua, 'a'*64)
|
||||
with self.assertRaisesRegex(ValueError, 'JPEG screenshot requires conversion record'):
|
||||
check.screenshot_pixels('/raw.png')
|
||||
|
||||
|
||||
class RestAuditTests(unittest.TestCase):
|
||||
def fixture(self):
|
||||
member = dict(staff_id='s', state='OFF_DUTY', health=100, current_task_id='', current_cell=[0, 0, 0])
|
||||
facilities = [dict(instance_id=fid, definition_id='staff_waiting_room', origin=[0, 0, 0],
|
||||
occupied_cells=[], state='OPEN', suspension_reason='') for fid in ('a', 'b')]
|
||||
runtime = dict(staff=dict(staff=[member]), facilities=dict(instances=facilities),
|
||||
defense=dict(raids=[]), events=dict(actor_locks={}), elevators=dict(trips=[]),
|
||||
path=dict(cell_traffic=[dict(cell=[x, 0, 0]) for x in (-1, 0, 1)]),
|
||||
world=dict(bounds=dict(min_x=-1, max_x=1, min_y=0, max_y=0), occupancies=[]))
|
||||
routes = [dict(cell=[x, 0, 0], path=[[0, 0, 0], [x, 0, 0]] if x else [[0, 0, 0]],
|
||||
edges=[dict(**{'from': [0, 0, 0], 'to': [x, 0, 0]}, total_cost=0)] if x else [],
|
||||
calculate_path_cost=0) for x in (-1, 0, 1)]
|
||||
observed = []
|
||||
for f in facilities:
|
||||
observed.append({k: f[k] for k in ('instance_id', 'definition_id', 'origin', 'occupied_cells')})
|
||||
observed[-1].update(operation_state='OPERATING', probes=[dict(cell=[x, 0, 0], has_cell=True) for x in (0, -1, 1)],
|
||||
ordered=[[x, 0, 0] for x in (-1, 0, 1)], routes=copy.deepcopy(routes))
|
||||
selected = dict(reachable=True, resting=True, cell=[-1, 0, 0], facility_id='a', label='待機室で休憩中')
|
||||
audit = dict(path_service_sha256=check.sha(check.ROOT/'scripts/world/path_service.gd'),
|
||||
staff=[dict(member, defense_memberships=[], actor_lock=False, in_transit=False,
|
||||
facilities=observed, comparison_only=selected)])
|
||||
report = dict(resting_staff={'s': 'a'}, actor_drawings=[], layer=0, carried_patients={}, rooms={},
|
||||
viewport=[1440, 810], zoom=1, pan=[0, 0])
|
||||
return dict(systems=dict(runtime=runtime)), report, audit
|
||||
|
||||
def test_selection_ties_and_nonmutation(self):
|
||||
state, report, audit = self.fixture()
|
||||
before = copy.deepcopy((state, report, audit))
|
||||
self.assertEqual(check.compare_rest(state, report, audit)['s']['cell'], [-1, 0, 0])
|
||||
self.assertEqual((state, report, audit), before)
|
||||
|
||||
def test_rest_forgeries(self):
|
||||
mutations = {
|
||||
'selection': lambda a: a['staff'][0]['comparison_only'].update(facility_id='b'),
|
||||
'tie': lambda a: a['staff'][0]['comparison_only'].update(cell=[0, 0, 0]),
|
||||
'lock': lambda a: a['staff'][0].update(actor_lock=True),
|
||||
'transit': lambda a: a['staff'][0].update(in_transit=True),
|
||||
'label': lambda a: a['staff'][0]['comparison_only'].update(label='宿舎で休憩中'),
|
||||
'cost': lambda a: a['staff'][0]['facilities'][0]['routes'][0]['edges'][0].update(total_cost=9),
|
||||
'facility_order': lambda a: a['staff'][0]['facilities'].reverse(),
|
||||
'candidate_order': lambda a: a['staff'][0]['facilities'][0]['ordered'].reverse(),
|
||||
'empty_edge': lambda a: a['staff'][0]['facilities'][0]['routes'][0]['edges'][0].clear(),
|
||||
}
|
||||
for name, mutate in mutations.items():
|
||||
with self.subTest(name=name):
|
||||
state, report, audit = self.fixture()
|
||||
mutate(audit)
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_rest(state, report, audit)
|
||||
|
||||
def test_ineligible_and_transit_recomputed(self):
|
||||
state, report, audit = self.fixture()
|
||||
state['systems']['runtime']['events']['actor_locks']['s'] = 'event'
|
||||
audit['staff'][0].update(actor_lock=True, comparison_only={})
|
||||
report['resting_staff'] = {}
|
||||
self.assertEqual(check.compare_rest(state, report, audit), {'s': {}})
|
||||
state, report, audit = self.fixture()
|
||||
state['systems']['runtime']['elevators']['trips'] = [dict(actor_id='staff:s')]
|
||||
audit['staff'][0]['in_transit'] = True
|
||||
audit['staff'][0]['comparison_only'].update(resting=False, label='休憩先へ移動中')
|
||||
report['resting_staff'] = {}
|
||||
self.assertFalse(check.compare_rest(state, report, audit)['s']['resting'])
|
||||
|
||||
|
||||
class InfrastructureTests(unittest.TestCase):
|
||||
def test_matrix_requires_each_condition_and_each_category(self):
|
||||
executed = {c: {k: set(v) for k, v in categories.items()} for c, categories in check.REQUIRED_CHECKS.items()}
|
||||
self.assertTrue(all(r['status'] == 'passed' for r in check.condition_matrix(executed)))
|
||||
for condition in check.CONDITIONS:
|
||||
for category in check.CATEGORIES:
|
||||
with self.subTest(condition=condition, category=category):
|
||||
broken = copy.deepcopy(executed)
|
||||
broken[condition][category].clear()
|
||||
matrix = check.condition_matrix(broken)
|
||||
self.assertEqual(next(r for r in matrix if r['condition'] == condition)['status'], 'pending')
|
||||
self.assertIn(category, check.BLOCKERS(broken)[condition])
|
||||
|
||||
def test_distinct_pid_ack_and_formal_hash_negatives(self):
|
||||
members = {name: 'a'*64 for name in ('world.bin', 'manifest.json', 'thumbnail.png', 'journal.log')}
|
||||
writer = dict(actual_pid=1)
|
||||
reader = dict(actual_pid=2, formal_input={k: dict(sha256=v) for k, v in members.items()})
|
||||
saved = dict(pid=1, members=members)
|
||||
records = [dict(request=dict(action='load_menu', target_pid=2, sequence=1, requested_ticks_usec=10, requested_unix_ns=90),
|
||||
ack=dict(target_pid=2, sequence=1, request_sha256='b'*64, timestamp_ns=110),
|
||||
observed=dict(pid=2, sequence=1, request_sha256='b'*64, ack_observed_ticks_usec=20, ack_observed_unix_ns=120, status='手動セーブを読み込みました。'),
|
||||
cua=dict(target_pid=2, method='native_os', tool_call_id='actual-call', timestamp_ns=100),
|
||||
request_sha256='b'*64)]
|
||||
check.inspect_resume(writer, reader, saved, records, members)
|
||||
for change in ('pid', 'ack', 'time', 'hash', 'id'):
|
||||
with self.subTest(change=change):
|
||||
r, q = copy.deepcopy(reader), copy.deepcopy(records)
|
||||
if change == 'pid': r['actual_pid'] = 1
|
||||
if change == 'ack': q.clear()
|
||||
if change == 'time': q[0]['observed']['ack_observed_ticks_usec'] = 9
|
||||
if change == 'hash': r['formal_input']['world.bin']['sha256'] = 'c'*64
|
||||
if change == 'id': q[0]['ack']['sequence'] = 2
|
||||
with self.assertRaises(ValueError): check.inspect_resume(writer, r, saved, q, members)
|
||||
|
||||
def test_interpolation_history_tampering(self):
|
||||
state = dict(systems=dict(runtime=dict(guests=dict(guests=[]), staff=dict(staff=[]),
|
||||
elevators=dict(shafts=[], trips=[], current_minute=0))))
|
||||
report = dict(animation_time=0.0, paused=True, layer=0, zoom=1, pan=[0,0], viewport=[100,100],
|
||||
actor_tracks={}, elevator_tracks={}, actor_drawings=[], cabins=[])
|
||||
audit = dict(frames=[dict(frame=1, delta=.01, scaled_delta=0, display=copy.deepcopy(report)),
|
||||
dict(frame=2, delta=.01, scaled_delta=0, display=copy.deepcopy(report))])
|
||||
check.compare_motion(state, report, audit)
|
||||
for change in ('delta', 'frame', 'decreasing_frame', 'no_frames', 'one_frame', 'display', 'report_binding'):
|
||||
with self.subTest(change=change):
|
||||
bad = copy.deepcopy(audit)
|
||||
if change == 'delta': bad['frames'][1]['scaled_delta'] = 1
|
||||
if change == 'frame': bad['frames'][1]['frame'] = 1
|
||||
if change == 'decreasing_frame': bad['frames'][1]['frame'] = 0
|
||||
if change == 'no_frames': bad['frames'].clear()
|
||||
if change == 'one_frame': bad['frames'].pop()
|
||||
if change == 'report_binding': bad['frames'][1]['display']['zoom'] = 2
|
||||
if change == 'display': bad['frames'][1]['display']['animation_time'] = 1
|
||||
with self.assertRaises(ValueError): check.compare_motion(state, report, bad)
|
||||
|
||||
def test_verify_only_missing_index_is_a_concrete_deficit(self):
|
||||
with mock.patch.dict(check.os.environ, {'BUILD_OUTPUT_DIR': '/missing-p23-proof'}, clear=True):
|
||||
with contextlib.redirect_stdout(io.StringIO()) as output:
|
||||
self.assertEqual(check.main(['--verify-only']), 1)
|
||||
result = json.loads(output.getvalue())
|
||||
self.assertEqual(result['integrity'], 'fail')
|
||||
self.assertEqual(result['acceptance'], 'pending')
|
||||
self.assertIn('index.json', result['deficits'][0])
|
||||
self.assertNotIn('blocked', result['deficits'][0])
|
||||
|
||||
def test_verify_only_requires_build_output(self):
|
||||
with mock.patch.dict(check.os.environ, {}, clear=True):
|
||||
with contextlib.redirect_stderr(io.StringIO()) as output:
|
||||
self.assertEqual(check.main(['--verify-only']), 2)
|
||||
self.assertIn('BUILD_OUTPUT_DIR is required', output.getvalue())
|
||||
|
||||
def test_normative_plan_change_invalidates_but_progress_does_not(self):
|
||||
root = check.ROOT
|
||||
text = (root / check.PLAN).read_text()
|
||||
original = check.input_map(root)
|
||||
progress = text.splitlines(keepends=True)
|
||||
progress[2] = 'Progress-only test mutation\n'
|
||||
with mock.patch.object(Path, 'read_text', return_value=''.join(progress)):
|
||||
self.assertEqual(check.input_map(root), original)
|
||||
changed = text.replace('同需要の動線改善を観察できる。', 'altered normative acceptance')
|
||||
self.assertNotEqual(changed, text)
|
||||
with mock.patch.object(Path, 'read_text', return_value=changed):
|
||||
self.assertNotEqual(check.input_map(root), original)
|
||||
self.assertNotIn(check.PLAN, original)
|
||||
self.assertIn('full_plan_sha256', check.input_provenance(root))
|
||||
for path in (root / 'tests/fixtures/p23-03').rglob('*'):
|
||||
if path.is_file():
|
||||
self.assertIn(path.relative_to(root).as_posix(), original)
|
||||
|
||||
def test_os_process_guards_reject_engine_profile_and_source_drift(self):
|
||||
source = {'source.gd': 'a' * 64}
|
||||
receipt = dict(engine_ended=True, passed=True, exit_code=0, error=None,
|
||||
guard_errors=[], cleanup_errors=[], default_before={}, default_after={},
|
||||
stage_before={}, stage_after={}, source_map=source,
|
||||
source_after={'source.gd': {'sha256': 'a' * 64}},
|
||||
owned_cleanup=[{'engine_ended': True}], ordinary_owner_proof={'present': True})
|
||||
receipt.update(spec_sha256=check.SPEC_SHA, inputs={'spec': 'a'*64}, inputs_after={'spec': 'a'*64},
|
||||
environment={'P23_LAYOUT': 'A'}, environment_after={'P23_LAYOUT': 'A'}, actual_pid=123,
|
||||
profile='/private/test', profile_name='test', profile_before={'existed': False, 'files': {}}, profile_after={})
|
||||
for field in ('engine_sha256', 'settings_sha256', 'environment_sha256'):
|
||||
receipt[field] = receipt[field+'_after'] = 'a'*64
|
||||
check.inspect_os_receipt(receipt, source)
|
||||
for field, value in [('engine_ended', False), ('default_after', {'changed': True}),
|
||||
('source_after', {'source.gd': {'sha256': 'b' * 64}}),
|
||||
('stage_after', {'changed': True}), ('owned_cleanup', []),
|
||||
('environment_after', {}), ('profile_before', {}),
|
||||
('engine_sha256_after', 'b'*64), ('spec_sha256', 'b'*64)]:
|
||||
with self.subTest(field=field):
|
||||
altered = copy.deepcopy(receipt)
|
||||
altered[field] = value
|
||||
with self.assertRaises(ValueError):
|
||||
check.inspect_os_receipt(altered, source)
|
||||
|
||||
|
||||
class SemanticTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.guest = dict(guest_id='g', state='QUEUING', current_cell=[1, 0, 0],
|
||||
target_facility_id='f', current_service_id='', queue_request_id='q')
|
||||
self.staff = dict(staff_id='s', state='WORKING', current_cell=[2, 0, 1],
|
||||
current_task_id='t', assigned_facility_id='f', role_id='porter',
|
||||
carried_inventory_id='cargo')
|
||||
self.task = dict(task_id='t', state='WORKING', task_type='delivery',
|
||||
assigned_staff_id='s', target_entity_id='request', rescue_facility_id='')
|
||||
self.snapshot = {'systems': {'runtime': {'guests': {'guests': [self.guest]},
|
||||
'staff': {'staff': [self.staff], 'tasks': [self.task]}}}}
|
||||
self.report = dict(guests=[dict(id='g', state='QUEUING', cell=[1, 0, 0], target='f', service='', queue='q')],
|
||||
staff=[dict(id='s', state='WORKING', cell=[2, 0, 1], task='t', facility='f',
|
||||
role='porter', carried_inventory_id='cargo')],
|
||||
tasks=[dict(id='t', state='WORKING', type='delivery', staff='s',
|
||||
target='request', rescue_facility='')])
|
||||
|
||||
def test_real_schema_projection(self):
|
||||
self.assertEqual(check.compare_actor_rows(self.snapshot, self.report), ['guests', 'staff', 'tasks'])
|
||||
|
||||
def test_changed_state_position_target_task_and_goods_are_rejected(self):
|
||||
for category, field, replacement in [('guests', 'cell', [8, 0, 0]), ('guests', 'target', 'other'),
|
||||
('guests', 'state', 'MOVING'), ('staff', 'task', ''),
|
||||
('staff', 'carried_inventory_id', ''), ('tasks', 'staff', 'other')]:
|
||||
with self.subTest(category=category, field=field):
|
||||
report = copy.deepcopy(self.report)
|
||||
report[category][0][field] = replacement
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_actor_rows(self.snapshot, report)
|
||||
|
||||
def test_missing_extra_and_boolean_only_rows_are_rejected(self):
|
||||
for bad in [[], self.report['guests'] * 2, [True]]:
|
||||
report = copy.deepcopy(self.report)
|
||||
report['guests'] = bad
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_actor_rows(self.snapshot, report)
|
||||
|
||||
def test_unknown_enum_and_duplicate_id_are_rejected(self):
|
||||
self.guest['state'] = 'FICTIONAL'
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_actor_rows(self.snapshot, self.report)
|
||||
self.guest['state'] = 'QUEUING'
|
||||
self.snapshot['systems']['runtime']['guests']['guests'].append(copy.deepcopy(self.guest))
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_actor_rows(self.snapshot, self.report)
|
||||
|
||||
def test_typed_rectangle_and_duplicate_vector_key(self):
|
||||
self.assertEqual(check.decode(dict(type='Rect2', text='Rect2(1, 2, 16, 24)')), [1, 2, 16, 24])
|
||||
with self.assertRaises(ValueError):
|
||||
check.decode(dict(type='Rect2', text='Rect2(1, nan, 16, 24)'))
|
||||
key = dict(type='Vector3i', text='Vector3i(1, 2, 0)')
|
||||
value = dict(type='int', text='1')
|
||||
with self.assertRaises(ValueError):
|
||||
check.decode(dict(type='Dictionary', entries=[[key, value], [key, value]]))
|
||||
|
||||
def test_immutable_typed_input_is_not_modified(self):
|
||||
before = copy.deepcopy(self.snapshot)
|
||||
check.compare_actor_rows(self.snapshot, self.report)
|
||||
self.assertEqual(before, self.snapshot)
|
||||
|
||||
|
||||
class SpatialTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.runtime = dict(world=dict(bounds=dict(min_x=0, max_x=1, min_y=0, max_y=0),
|
||||
walkable_cells=[[0, 0, 0], [1, 0, 0]], occupancies=[]),
|
||||
path=dict(cell_traffic=[dict(cell=[0, 0, 0], base_capacity=2,
|
||||
current_density=3, dynamic_cost=0, temporary_block=False)]),
|
||||
facilities=dict(instances=[], requests=[]), guests=dict(guests=[]),
|
||||
staff=dict(staff=[], tasks=[]), logistics_requests=[],
|
||||
elevators=dict(current_minute=0, shafts=[], trips=[]))
|
||||
self.snapshot = dict(systems=dict(runtime=self.runtime))
|
||||
self.report = dict(viewport=[184, 164], zoom=1, pan=[0, 0], layer=0,
|
||||
traffic=[dict(cell=[0, 0, 0], rect=[78, 72, 12, 20],
|
||||
traffic=dict(base_capacity=2, current_density=3,
|
||||
dynamic_cost=0, temporary_block=False))],
|
||||
queue_positions={}, queue_hidden={}, landing_badges=[], rooms={},
|
||||
treated_patients={}, carried_patients={}, resting_staff={},
|
||||
goods=dict(requests=[], deliveries=[]))
|
||||
|
||||
def test_density_and_pan_are_independently_recomputed(self):
|
||||
check.compare_traffic(self.snapshot, self.report)
|
||||
for key, value in [('current_density', 0), ('base_capacity', 9)]:
|
||||
changed = copy.deepcopy(self.report)
|
||||
changed['traffic'][0]['traffic'][key] = value
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_traffic(self.snapshot, changed)
|
||||
self.report['pan'][0] = 20
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_traffic(self.snapshot, self.report)
|
||||
|
||||
def test_missing_queue_overflow_rejected(self):
|
||||
saved = json.loads((check.ROOT / 'tests/fixtures/p23-03/queued_elevator.json').read_text())['saved']
|
||||
self.runtime['elevators'] = saved
|
||||
saved['trips'] = [dict(copy.deepcopy(saved['trips'][0]), actor_id='guest:g'+str(i),
|
||||
request_id='trip'+str(i), enqueued_at=i) for i in range(3)]
|
||||
self.report.update(queue_positions={'guest/g0': [100, 82]},
|
||||
queue_hidden={'guest/g1': True, 'guest/g2': True}, landing_badges=[dict(at=[-4, 54],
|
||||
text='待ち3人(他2人)', cell=[0, 0, 0],
|
||||
members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=1, overflow=2)])
|
||||
check.compare_queues(self.snapshot, self.report)
|
||||
self.report['landing_badges'][0]['overflow'] = 0
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_queues(self.snapshot, self.report)
|
||||
|
||||
def test_fictional_goods_and_rooms_rejected(self):
|
||||
check.compare_goods(self.snapshot, self.report)
|
||||
check.compare_room_services(self.snapshot, self.report)
|
||||
self.report['goods']['requests'].append(dict(stock=100))
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_goods(self.snapshot, self.report)
|
||||
self.report['rooms']['invented'] = dict(count=0)
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_room_services(self.snapshot, self.report)
|
||||
|
||||
def test_room_reason_and_rescue_occupancy(self):
|
||||
room = dict(instance_id='clinic', origin=[0, 0, 0], definition_id='infirmary',
|
||||
state='NO_POWER', state_reasons=['NO_POWER'], suspension_reason='',
|
||||
customer_capacity=2, active_staff_ids=[], active_services=[], occupied_cells=[])
|
||||
self.runtime['facilities']['instances'] = [room]
|
||||
self.runtime['staff']['tasks'] = [dict(task_id='rescue', task_type='rescue_transport',
|
||||
state='WORKING', assigned_staff_id='carrier', target_entity_id='patient', rescue_facility_id='clinic')]
|
||||
self.report['treated_patients'] = {'patient': 'clinic'}
|
||||
self.report['rooms'] = {'clinic': dict(state=2, state_name='NO_POWER', reasons=['NO_POWER'],
|
||||
operation=0, people=['patient'], count=1, limit=2, staff=[], suspension='')}
|
||||
check.compare_room_services(self.snapshot, self.report)
|
||||
for field, wrong in [('state', 6), ('operation', 1), ('people', []), ('count', 0)]:
|
||||
changed = copy.deepcopy(self.report)
|
||||
changed['rooms']['clinic'][field] = wrong
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_room_services(self.snapshot, changed)
|
||||
|
||||
def test_cabin_geometry_and_phase(self):
|
||||
shaft = dict(config=dict(shaft_id='lift', stops=[[0, 0, 0]], width=2, capacity=4),
|
||||
current_floor=0, phase='IDLE', operational=True, boarded=[])
|
||||
self.runtime['elevators']['shafts'] = [shaft]
|
||||
self.report.update(paused=True, animation_time=0, elevator_tracks={'lift': dict(floor=0, at=0)},
|
||||
cabins=[dict(shaft_id='lift', rect=[80, 72, 24, 19], pixel=1, operational=True,
|
||||
doors_open=True, badge_at=[80, 58], badge='0/4人', passengers=[])])
|
||||
check.compare_cabin_geometry(self.snapshot, self.report)
|
||||
for field, value in [('rect', [80, 70, 24, 19]), ('doors_open', False), ('badge', '4/4人')]:
|
||||
changed = copy.deepcopy(self.report)
|
||||
changed['cabins'][0][field] = value
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_cabin_geometry(self.snapshot, changed)
|
||||
|
||||
|
||||
class ComparisonTests(unittest.TestCase):
|
||||
def pair(self):
|
||||
declaration = dict(metric='sum_destination_arrival_minutes_entire_cohort',
|
||||
arrivals=[dict(id='g', offset=0)], window=2, to=[2, 1, 0],
|
||||
improvement=dict(definition_id='guest_elevator', origin=[4, 0, 0]))
|
||||
a = dict(declaration=declaration, initial=dict(seed=1))
|
||||
a['arrival-g'] = dict(offset=0, payload=dict(guest_id='g'), result=dict(code='OK'))
|
||||
for minute in range(3):
|
||||
a['tick-%03d' % minute] = dict(minute=minute, guests=dict(guests=[dict(guest_id='g',
|
||||
current_cell=[2, 1, 0] if minute == 2 else [2, 0, 0])]))
|
||||
b = copy.deepcopy(a)
|
||||
b['build-0'] = dict(payload=declaration['improvement'], result=dict(code='OK'))
|
||||
b['tick-001']['guests']['guests'][0]['current_cell'] = [2, 1, 0]
|
||||
return a, b
|
||||
|
||||
def test_strict_metric_from_typed_cells(self):
|
||||
a, b = self.pair()
|
||||
self.assertEqual(check.compare_layouts(a, b)['B'], 1)
|
||||
for mutation in ('demand', 'incomplete', 'no_improvement', 'extra_build'):
|
||||
with self.subTest(mutation=mutation):
|
||||
a, b = self.pair()
|
||||
if mutation == 'demand':
|
||||
b['arrival-g']['payload']['guest_id'] = 'other'
|
||||
elif mutation == 'incomplete':
|
||||
for minute in (1, 2):
|
||||
b['tick-%03d' % minute]['guests']['guests'][0]['current_cell'] = [2, 0, 0]
|
||||
elif mutation == 'no_improvement':
|
||||
b['tick-001'] = copy.deepcopy(a['tick-001'])
|
||||
else:
|
||||
b['build-1'] = copy.deepcopy(b['build-0'])
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_layouts(a, b)
|
||||
|
||||
|
||||
class OfflineGeometryTests(unittest.TestCase):
|
||||
"""Synthetic calibration examples; no OS or acceptance credit."""
|
||||
|
||||
def setUp(self):
|
||||
self.calibration = dict(window_size=[100, 80], window_position=[240, 150],
|
||||
screenshot_pixels=[200, 190], backing_scale=2,
|
||||
canvas_stretch=1, titlebar_pixels=30)
|
||||
self.request = dict(window_size=[100, 80], window_position=[240, 150],
|
||||
screen_scale=2, content_scale_factor=1, target_pid=123)
|
||||
self.clicks = [dict(target_pid=123, events=[dict(action='click',
|
||||
coordinates=[20, 70], calibration=copy.deepcopy(self.calibration),
|
||||
point=dict(screen=[10, 20], viewport=[10, 20],
|
||||
viewport_to_screen=[1, 0, 0, 1, 0, 0]))])]
|
||||
|
||||
def transform(self):
|
||||
return check.screenshot_transform(self.calibration, self.request, [200, 190], self.clicks)
|
||||
|
||||
def test_uses_measured_scale_origin_not_desktop_position(self):
|
||||
self.assertEqual(check.affine_rect(self.transform(), [10, 20, 5, 7]), [20, 70, 10, 14])
|
||||
|
||||
def test_shifted_titlebar_fails_independent_click(self):
|
||||
self.calibration['titlebar_pixels'] += 1
|
||||
self.calibration['screenshot_pixels'][1] += 1
|
||||
self.clicks[0]['events'][0]['calibration'] = copy.deepcopy(self.calibration)
|
||||
with self.assertRaises(ValueError):
|
||||
check.screenshot_transform(self.calibration, self.request, [200, 191], self.clicks)
|
||||
|
||||
def test_wrong_click_offset_fails(self):
|
||||
self.clicks[0]['events'][0]['coordinates'][1] -= 30
|
||||
with self.assertRaisesRegex(ValueError, 'click screenshot offset'):
|
||||
self.transform()
|
||||
|
||||
def test_wrong_pid_cannot_calibrate(self):
|
||||
self.clicks[0]['target_pid'] += 1
|
||||
with self.assertRaisesRegex(ValueError, 'missing independent click'):
|
||||
self.transform()
|
||||
|
||||
def test_nonfinite_and_false_scale_rejected(self):
|
||||
for scale in (False, float('nan'), 0, -1):
|
||||
self.calibration['backing_scale'] = scale
|
||||
with self.assertRaisesRegex(ValueError, 'invalid screenshot calibration'):
|
||||
self.transform()
|
||||
|
||||
def test_all_visibility_cases_are_accounted_for(self):
|
||||
self.assertEqual(check.rectangle_eligibility([2, 3, 4, 5], [10, 10]),
|
||||
('visible', [2, 3, 4, 5]))
|
||||
self.assertEqual(check.rectangle_eligibility([-2, 3, 4, 5], [10, 10]),
|
||||
('clipped', [0, 3, 2, 5]))
|
||||
self.assertEqual(check.rectangle_eligibility([10, 3, 4, 5], [10, 10])[0], 'outside')
|
||||
self.assertEqual(check.rectangle_eligibility([2, 3, 0, 5], [10, 10])[0], 'outside')
|
||||
|
||||
def test_invalid_rectangles_rejected(self):
|
||||
for rect in ([0, 0, -1, 4], [float('nan'), 0, 1, 4]):
|
||||
with self.assertRaisesRegex(ValueError, 'invalid clipping rectangle'):
|
||||
check.rectangle_eligibility(rect, [10, 10])
|
||||
|
||||
def test_noise_is_measured_and_does_not_assert_class(self):
|
||||
before = (2, 2, [[(10, 20, 30)]*2 for _ in range(2)])
|
||||
after = copy.deepcopy(before)
|
||||
after[2][1][1] = (10, 27, 30)
|
||||
noise = check.paired_crop_noise(before, after, [0, 0, 2, 2])
|
||||
self.assertEqual(noise, dict(samples=4, max_channel_delta=7,
|
||||
mean_channel_delta=1.75, unchanged=3))
|
||||
self.assertNotIn('pass', noise)
|
||||
self.assertNotIn('state', noise)
|
||||
|
||||
def test_wrong_dimensions_and_empty_crop_rejected(self):
|
||||
image = (1, 1, [[(0, 0, 0)]])
|
||||
with self.assertRaisesRegex(ValueError, 'paired image dimensions'):
|
||||
check.paired_crop_noise(image, (2, 1, []), [0, 0, 1, 1])
|
||||
with self.assertRaisesRegex(ValueError, 'empty noise crop'):
|
||||
check.paired_crop_noise(image, image, [1, 0, 1, 1])
|
||||
|
||||
def test_report_positions_and_source_provenance_are_recomputed(self):
|
||||
source = 'scripts/ui/pixel_people.gd'
|
||||
report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[],
|
||||
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1)])
|
||||
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||
(check.ROOT/source).read_text())))
|
||||
audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18],
|
||||
style_source=source, style_colors=colors)])
|
||||
check.validate_drawing_rows(report, audit)
|
||||
for change in ('shift', 'identity', 'missing', 'palette'):
|
||||
tampered = copy.deepcopy(audit)
|
||||
if change == 'shift': tampered['rows'][0]['rect'][0] += 1
|
||||
if change == 'identity': tampered['rows'][0]['category'] = 'guest'
|
||||
if change == 'missing': tampered['rows'] = []
|
||||
if change == 'palette': tampered['rows'][0]['style_colors'] = ['000000']
|
||||
with self.subTest(change=change), self.assertRaises(ValueError):
|
||||
check.validate_drawing_rows(report, tampered)
|
||||
|
||||
|
||||
class FrameReviewTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
OfflineGeometryTests.setUp(self)
|
||||
self.request.update(local_to_viewport=[1, 0, 0, 1, 0, 0],
|
||||
viewport_to_screen=[1, 0, 0, 1, 0, 0],
|
||||
local_to_screen=[1, 0, 0, 1, 0, 0])
|
||||
self.report = dict(layer=0, viewport=[100, 80], rooms={}, cabins=[],
|
||||
landing_badges=[], badges=[], resting_staff={}, traffic=[],
|
||||
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1, state=0)])
|
||||
source = 'scripts/ui/pixel_people.gd'
|
||||
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||
(check.ROOT/source).read_text())))
|
||||
self.audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18],
|
||||
viewport_rect=[1, 1, 16, 18], screen_rect=[1, 1, 16, 18],
|
||||
style_source=source, style_colors=colors)])
|
||||
self.image = (200, 190, [[(x % 256, y % 256, 0) for x in range(200)] for y in range(190)])
|
||||
|
||||
def frame(self):
|
||||
return check.frame_rows(self.report, self.audit, self.request,
|
||||
dict(calibration=self.calibration), [self.image], self.clicks)
|
||||
|
||||
def test_blank_crop_fails_without_threshold(self):
|
||||
self.image = (200, 190, [[(0, 0, 0)] * 200 for _ in range(190)])
|
||||
result = self.frame()
|
||||
self.assertFalse(result['mechanical_pass'])
|
||||
self.assertIn('blank crop', result['rows'][0]['failures'])
|
||||
|
||||
def test_missing_extra_and_unaudited_traffic_fail(self):
|
||||
for change in ('missing', 'extra', 'traffic'):
|
||||
with self.subTest(change=change):
|
||||
original = copy.deepcopy(self.audit)
|
||||
if change == 'missing': self.audit['rows'].clear()
|
||||
if change == 'extra': self.audit['rows'] *= 2
|
||||
if change == 'traffic':
|
||||
self.report['traffic'] = [dict(cell=[-6, 2, 0], rect=[1, 1, 5, 5],
|
||||
traffic=dict(current_density=1, temporary_block=False))]
|
||||
result = self.frame()
|
||||
self.assertFalse(result['mechanical_pass'])
|
||||
self.assertEqual(len(result['rows']), len(self.audit['rows']))
|
||||
self.assertIn('offline rectangle coverage', result['errors'])
|
||||
self.audit = original
|
||||
|
||||
def test_dense_or_blocked_traffic_row_closes_audit_gap(self):
|
||||
source = 'scenes/main/world_preview.gd'
|
||||
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||
(check.ROOT/source).read_text())))
|
||||
for density, blocked in ((1, False), (0, True), (0, False)):
|
||||
with self.subTest(density=density, blocked=blocked):
|
||||
self.report['traffic'] = [dict(cell=[-6, 2, 0], rect=[1, 1, 5, 5],
|
||||
traffic=dict(current_density=density, temporary_block=blocked))]
|
||||
active = density > 0 or blocked
|
||||
missing = self.frame()
|
||||
self.assertEqual(missing['mechanical_pass'], not active)
|
||||
if active:
|
||||
self.assertIn('offline rectangle coverage', missing['errors'])
|
||||
self.audit['rows'].append(dict(category='traffic', id='-6,2,0',
|
||||
rect=[1, 1, 5, 5], viewport_rect=[1, 1, 5, 5],
|
||||
screen_rect=[1, 1, 5, 5], style_source=source, style_colors=colors))
|
||||
complete = self.frame()
|
||||
self.assertTrue(complete['mechanical_pass'], complete['errors'])
|
||||
self.audit['rows'].pop()
|
||||
|
||||
def test_wrong_state_enum_and_name_fail(self):
|
||||
source = 'scripts/ui/pixel_tower_art.gd'
|
||||
self.report['actor_drawings'] = []
|
||||
room = dict(rect=[1, 1, 16, 18], state=3, state_name='NO_STAFF', operation=0)
|
||||
self.report['rooms'] = {'r': room}
|
||||
self.audit['rows'][0].update(category='room', id='r', style_source=source,
|
||||
style_colors=list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||
(check.ROOT/source).read_text()))))
|
||||
self.assertEqual(self.frame()['rows'][0]['operation'], 'INACTIVE')
|
||||
for state, name in ((99, 'NO_STAFF'), (3, 'OPEN'), (True, 'NO_ACCESS')):
|
||||
room.update(state=state, state_name=name)
|
||||
with self.assertRaises(ValueError): self.frame()
|
||||
|
||||
def test_frame_rejects_affine_and_calibration_shift(self):
|
||||
self.request['local_to_screen'][4] += 1
|
||||
with self.assertRaisesRegex(ValueError, 'affine composition'): self.frame()
|
||||
self.request['local_to_screen'][4] -= 1
|
||||
self.clicks[0]['events'][0]['coordinates'][1] += 1
|
||||
with self.assertRaisesRegex(ValueError, 'click screenshot offset'): self.frame()
|
||||
|
||||
def test_eligible_crop_fully_outside_image_fails(self):
|
||||
for key in ('viewport_to_screen', 'local_to_screen'):
|
||||
self.request[key][4] = 300
|
||||
self.audit['rows'][0]['screen_rect'][0] += 300
|
||||
result = self.frame()
|
||||
self.assertFalse(result['mechanical_pass'])
|
||||
self.assertIn('eligible crop outside image', result['rows'][0]['failures'])
|
||||
|
||||
def test_manifest_crop_and_independent_review_binding(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
manifest, digest = check.write_frame_review(directory, self.frame(),
|
||||
[dict(whole_image_sha256='a'*64, raw_sha256='b'*64, conversion_sha256='c'*64)])
|
||||
self.assertEqual(check.sha(directory/'manifest.json'), digest)
|
||||
crop = manifest['rows'][0]['crops'][0]
|
||||
self.assertEqual(check.sha(directory/crop['path']), crop['sha256'])
|
||||
self.assertEqual(check.png_pixels((directory/crop['path']).read_bytes())[:2], (32, 36))
|
||||
verdict_path = directory/'independent-verdict.json'
|
||||
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set())
|
||||
verdict = dict(manifest_sha256=digest, reviewer='independent-opus-direct-image',
|
||||
rows=[dict(row_index=0, category=True, state=True, position=True)])
|
||||
for change in ('wrong_sha', 'missing_row', 'extra_row', 'rejected_state'):
|
||||
bad = copy.deepcopy(verdict)
|
||||
if change == 'wrong_sha': bad['manifest_sha256'] = '0'*64
|
||||
if change == 'missing_row': bad['rows'] = []
|
||||
if change == 'extra_row': bad['rows'] *= 2
|
||||
if change == 'rejected_state': bad['rows'][0]['state'] = False
|
||||
verdict_path.write_text(json.dumps(bad))
|
||||
with self.subTest(change=change), self.assertRaises(ValueError):
|
||||
check.reviewed_pixel_categories(manifest, digest, verdict_path)
|
||||
verdict_path.write_text(json.dumps(verdict))
|
||||
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), {'staff'})
|
||||
manifest['mechanical_pass'] = False
|
||||
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||