WIP: feat(control): stream sampled game state through the bridge #58

Draft
mooki wants to merge 8 commits from mooki/Longshot:play-harness into main
First-time contributor

Why

A script steering Link needs current game state and the frame at which it was sampled, not an undated snapshot or a second output file. This adds read-only samples to the existing control bridge; kept as a draft here because quiet off/on frame-time cost has not been measured, despite the functional Longshot run below.

The original https://github.com/NotRexed/Hookshot/issues/2292 was approved by Joey-Magnus. His latest review says both requested changes were settled: bound the table lookup and use the existing bridge instead of a parallel file channel. That approval predates the later age-command rebase and this move; it is not a new forge approval.

What changed

  • host/oot_state_tap.c reads bounded guest values only in Link's update under the existing debug lock. Copied JSON crosses to the socket thread; it does not follow guest pointers there.
  • status.game carries 14 core fields, including sample frame/time, position, camera, life and the existing event/loading/age values. Unknown values are null. A retained sample during pause/loading is not fresh control.
  • Opt-in oot subscriptions have independent cadence, watched flags and change baselines. At most 64 watched flags and 32 reported changes; both lookup and scan reject tables larger than 2^22 slots. Pending samples are not overwritten before consumption.
  • Disconnect/replacement/unsubscribe clear the old watch. Protocol 1, ordinary state events and default subscriptions stay unchanged. The old file channel and its four switches are gone.
  • The renamed client exposes the snapshot and watch --events oot --every 6 --flags ... --changes. Main's typed age-command accessor stays intact; the JSON accessor is separately named oot_sample.
  • New local fixture adaptation: the two C fixture compiler commands define _CRT_SECURE_NO_WARNINGS, matching the repository's UTF-8 fixture convention. This applies the CRT declaration policy to Windows-target fixture compilation while retaining -Werror and every behavioral assertion. No native runtime adaptation was needed beyond the project rename.

Compared with GitHub head f2970916a626dc9f7a3eb1079063881c1687da2d, all four host files, the client, tests/control_test.c and the client tests are byte-identical after the mechanical rename. Only tests/test_control.py and tests/test_oot_state_tap.py add that compiler definition; documentation adds current evidence. The historical fork checks passed the control/client checks on both systems, but the whole run had then-known unrelated failures; it was not globally green and is not new Windows evidence.

Overlap with another open pull request: this and pull request 47 (command-line test sessions) both change scripts/longshot_ctl.py, tests/test_longshot_ctl.py and the results table. Each merges cleanly into main on its own; merged together they conflict in those three files, so whichever goes second needs a rebase, which I will do.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_CONTROL Extended existing switch: Enable copied core sample and opt-in oot events; no guest reads when off Unset/off Off by default: debug/control channel Linux synthetic-guest and Longshot forest session Ordinary play without control Fresh Windows, other GPUs; overhead (should help on: Controller-free tests on other platforms)
watch --events Extended: Accept oot event alongside existing event kinds present,state; log alone selects log Off by default: oot must be requested Linux offline checks Unrelated game/rendering behavior Live services and Windows (should help on: Same protocol on Windows)
watch --every Extended application: Cadence of requested oot samples in game presents 60 presents Off by default: caller opts into denser samples Linux offline checks Unrelated game/rendering behavior Live services and Windows (should help on: Same protocol on Windows)
watch --flags New: Up to 64 named or explicit-hash values in oot samples Empty watch list Off by default: bounded opt-in reads Linux offline checks Unrelated game/rendering behavior Live services and Windows (should help on: Same protocol on Windows)
watch --changes New: Track Bool/Int/Enum/UInt changes per subscription False Off by default: scans have a cost Linux offline checks Unrelated game/rendering behavior Live services and Windows (should help on: Same protocol on Windows)
tests --source-root New fixture selector: Select production source for negative-check comparisons Test checkout Off by default: test-only override Linux offline checks Unrelated game/rendering behavior Live services and Windows (should help on: Same protocol on Windows)
test_oot_state_tap.py --cc New fixture selector: Choose the C fixture compiler gcc Off by default: platform testing override Linux offline checks Unrelated game/rendering behavior Live services and Windows (should help on: Same protocol on Windows)
test_oot_state_tap.py --mutate New fixture selector: Inject a named regression into temporary source only None Off by default: negative-test mode Linux offline checks Unrelated game/rendering behavior Live services and Windows (should help on: Same protocol on Windows)
-D_CRT_SECURE_NO_WARNINGS New test build flag: Declare portable CRT use before fixture includes; keep Werror Defined for both fixture compile commands On by default: portable fixture contract Linux offline checks Unrelated game/rendering behavior Live services and Windows (should help on: Same protocol on Windows)
HOS_OOT_STATE_FILE Historical prototype only; not carried: Ignored: the file channel is replaced by the bridge Unset and ineffective Off by default: use the single bridge Legacy-switch rejection fixtures No file stream remains Native Windows execution (should help on: No live behavior intended)
HOS_OOT_STATE_EVERY Historical prototype only; not carried: Ignored: the file channel is replaced by the bridge Unset and ineffective Off by default: use the single bridge Legacy-switch rejection fixtures No file stream remains Native Windows execution (should help on: No live behavior intended)
HOS_OOT_STATE_FLAGS Historical prototype only; not carried: Ignored: the file channel is replaced by the bridge Unset and ineffective Off by default: use the single bridge Legacy-switch rejection fixtures No file stream remains Native Windows execution (should help on: No live behavior intended)
HOS_OOT_STATE_CHANGES Historical prototype only; not carried: Ignored: the file channel is replaced by the bridge Unset and ineffective Off by default: use the single bridge Legacy-switch rejection fixtures No file stream remains Native Windows execution (should help on: No live behavior intended)

Measurements

Branch state: on main d725b3631982ed0035ea3ac9cbe1677e9fad260c; head 365b02c32d0f46e208e3475b0811bd0001c203c5; whole game-free suite on this head: 511 passed, 1 failed, 20 skipped (main d725b3631982ed0035ea3ac9cbe1677e9fad260c: 510 passed, 1 failed, 20 skipped); the commits are unchanged by rebases apart from the results table.

The sole failure in both suites in the branch-state block is tests/test_updater_asset_digest.py: local .NET SDK 10.0.113 cannot resolve Microsoft.AspNetCore.App.Ref (= 10.0.13), error NU1100. The historical suites below also include this failure.

main: d4a69456c9 (2026-11-12)
this PR: play-harness @ c26c0d5f76

Measurement provenance: main @ d4a69456c9. c26c0d5f76, including the final documentation rows; native game build @ 4e63c76537.
Hardware: Core i9-13900K, 96 GB RAM, Arch Linux 7.2.15; RTX 4090 / NVIDIA 615.78.08 for the real game.
Scene: synthetic-guest/real-control-bridge fixtures; separate copied-save Kokiri Forest run, child Link standing, windowless, muted, 1280×720 captures, frame generation off, deferred shader preparation.
Method: same sampler driver before/after/before/after after the initial whole suite, fresh HOME, repository-default gcc. Median below is successful fixture completion, not speed. One ordinary shared Longshot session supplies runtime state and image evidence. No quiet off/on overhead measurement.

How to reproduce: Run python -I -B tests/test_oot_state_tap.py --source-root CHECKOUT with pinned main and PR checkouts, interleaved twice. For gameplay use a disposable copied Kokiri Forest save with HOS_CONTROL enabled, wait for 30 fresh controllable frames, subscribe to oot every 6 presents, read samples, press B for 8 frames, capture and quit. Use game.frame to date buffered samples.

Main d725b3631 adds only scripts, docs and tests, with no game-code change; the measured d4a69456 comparisons and pinned game evidence remain valid and were not repeated. Original results below retain their measured commits; the new suite is recorded in the branch-state block.

Baseline: main @ d4a69456

main @ d4a69456 this PR @ c26c0d5f
Pair 1: sampler fixture completes 0; sampler absent 1; complete fixture passes
Pair 2: sampler fixture completes 0; sampler absent 1; complete fixture passes
Median successful completion 0 1
Whole game-free suite 507 passed / 2 failed / 20 skipped 509 passed / 1 failed / 20 skipped
Windows-target fixture syntax Not repeated on main 4 translation units, 0 failures; not Windows execution
Before 1 and 2 (checkout prefix redacted):
FileNotFoundError: [Errno 2] No such file or directory: '<main checkout>/host/oot_state_tap.c'
After 1 and 2:
PASS: socket status, all typed flags, collision, nulls, 6/12/default-60 cadence, independent baselines
PASS: invalid subscription arguments are atomic; name/hash/count bounds and changes:false
PASS: replacement, non-OoT replacement, unsubscribe and disconnect clear pending samples and baselines
PASS: 32-change cap + more, moved/resized table baselines, adult and unreadable/non-finite fields
PASS: pending sample is retained, status stays current, and polling resumes without losing changes
PASS: 64 longest names, maximally escaped strings, changes and a 199-byte id form complete socket JSON
PASS: OoT socket sampler, preserved hash/constants/lock wiring, both table caps and no file switches
Final whole suite:
509 passed, 1 failed, 20 skipped in 280 s (linux, 6 at once)
PASS: 4 captured translation units, 0 failures, 0 unjudged drivers/files; Windows execution NOT TESTED

The before failure is feature absence, not a regression in main. The full suite ran on the exact final head above, after the last commit, with --jobs 6 --strict, fresh HOME and default compiler. Main's shared suite was run once for this day's moved pull requests on d4a69456. These checks have not yet been run on a forge runner.

Main also failed two tests/test_appimage_package_retry.py timing/sequence cases under heavy load; its requested isolated rerun passed all 9 tests. It fails only under heavy load on this PC; timing-dependent. This branch passed it. Neither full suite is described as all green.

The first Windows-target check reported missing CRT definitions for four translation units. After the separate fixture-only correction, all four passed. Native Windows execution of both the control and sampled-state fixtures remains untested.

Real Longshot session

Both native binaries were rebuilt from this branch, using a cloned immutable release-profile donor, with 0 failed compiles. Banner: longshot v2.2.0-rc.1-26-g4e63c7653 (source 20775991).

The standard-library session client started this game, waited for fresh controllable state sustained for 30 frames, read state, subscribed, pressed B, captured a picture and observed stop. Input came from a copied save; its input copy was unchanged. The 60.9 s run had load 33.94 → 51.29: not a performance comparison.

Selected fields from the actual replies:

{"state":"play","frame":1499,"game":{"frame":1499,"event":false,"loading":false,"age":"child"}}
{"event":"oot","game":{"frame":1500}}
{"event":"oot","game":{"frame":1506}}
{"ok":true,"frame":1514,"start":1506,"end":1514}
{"event":"oot","game":{"frame":1512}}
{"event":"oot","game":{"frame":1518}}
{"ok":true,"frame":1640,"present":1639,"width":1280,"height":720}
{"state":"finished","exit_code":0,"process_exited":true,"save_drain":"unsupported"}

All four samples carried all 14 core fields, at six-frame intervals. Sample 1512 was buffered during the press; 1518 is after its completion. Delivery order is not sample time. The inspected image shows Link, the forest, hearts and minimap. Fatal=0, crash=0, device-loss=0, kernel faults=0 and NVRM=0, with complete journal coverage. Save draining is unsupported.

Historical Hookshot evidence, not repeated or relabelled: the 2026-11-11 file/socket comparison on base a5b955fb, builds d847c0cf/a57f35fe, found nine standing fields equal and a watched Bool transition also in changed. Both exited 0. The socket picture was HUD on black while shaders remained queued, so no rendering-parity claim was made. A later Hookshot f2970916 run exercised child/adult state and the age command. Those are older builds, not this new run.

The complete suite was rerun after every source/documentation commit, with the default compiler and fresh HOME. The earlier measurement rows remain labelled by their actual commits. Main's suite was run once for all branches on this commit. Runner execution took 182 s on the branch, not a performance measurement. No hosted CI or new game run is claimed.

Testing

Tested: on this PC (Arch Linux, RTX 4090 with NVIDIA 615.78.08, i9-13900K), the current-head whole game-free suite and the earlier two interleaved main/branch sampler-fixture pairs at their measured commits above. The fixture covered socket status, typed flags, nulls, cadence, subscription bounds, independent baselines and cleanup; main's missing sampler was a feature-absence negative. Four translation units compiled for the Windows target; not run on Windows. In the Longshot Kokiri Forest run at native build 4e63c765, waited for fresh controllable play for 30 frames, read state, subscribed every 6 presents, pressed B for 8 frames, checked sample frames, took a screenshot and observed the game exit. The source save copy stayed unchanged; save draining remains unsupported.

Tested but not measured: The 60.9-second Longshot forest session and older Hookshot transport/age runs prove functionality, not sampling overhead, FPS or rendering parity.

Not tested: the explicitly listed platforms, integration and runtime cases below remain untested; only the bounded Linux/NVIDIA forest session above was run for Longshot, under shared load.

  • Quiet on/off frame-time cost, sampling overhead, long runs, a complete playthrough, real controllers, AMD/Intel/Steam Deck or a real window. No hardware test request was posted during this move.
  • The new run did not exercise age changes, map changes, pause/resume or flag transitions; those have historical or game-free evidence, not a new Longshot claim.
  • When HOS_CONTROL is unset the source returns before reads or allocation; this source fact is not a measured zero-cost claim. With changes enabled, scanning still costs work despite the output cap. Keep draft pending the feature template's off/on cost evidence.

Needs other testers:

  • Windows x64: build the host, run tests/test_control.py and tests/test_oot_state_tap.py, then subscribe to oot every 6 presents in controllable Kokiri Forest play and disconnect/reconnect, expecting passing fixtures, correctly dated samples and no stale watch after reconnect (request to open)

Screenshots

Nothing on screen is changed by the read-only sampler. main @ d4a69456 has no paired rendering capture; this PR @ 4e63c765 is the native build pictured below (runtime identical at c26c0d5f). No rendering-parity claim.

New Longshot branch run, Kokiri Forest: Link and HUD visible at present 1639 after the sampled-state/press sequence. Not a before/after rendering comparison.

Historical Hookshot file/socket evidence, 2026-11-11: left has Link; right is HUD on black with queued shaders. Its explicit rendering-parity limitation is retained.

Risk and rollback

A stale sample can mislead an automated controller; callers must require fresh game.frame and known-false event/loading. changes=true scans tables on the guest update under a lock and has unmeasured cost. Unset HOS_CONTROL to disable the sampler, unsubscribe from oot to remove watch scans, or revert the commit. This does not write guest memory or promise save-safe shutdown.

Reviewer notes

Start with hos_oot_state_tap and its early return, range checks, table caps and state_lock handoff; then connection cleanup and pending-sample handling in control.c. Keep main's typed oot_state age callback distinct from oot_sample. Quiet off/on cost and native Windows execution remain unmeasured. The live description's PR47 overlap warning is retained; whichever lands second still needs integration.

Issues

Original pull request and reviews: https://github.com/NotRexed/Hookshot/issues/2292.
Original tracking report: https://github.com/NotRexed/Hookshot/issues/2335, closed to consolidate discussion in that pull request.
No corresponding issue exists in the forge's public issue inventory checked for this move. No old GitHub number is used as a forge closing reference.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. Functional pairs are retained; quiet off/on feature cost remains unmeasured.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README). Relevant command/protocol details are in the topic guide; no unrelated menu default is added.
  • A row in docs/tested-configurations.md for each measured run.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Linux game built; Windows fixture syntax only, not a fresh full host build.
  • Docs updated.

Moved from NotRexed/Hookshot#2292.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra [email protected]

## Why A script steering Link needs current game state and the frame at which it was sampled, not an undated snapshot or a second output file. This adds read-only samples to the existing control bridge; **kept as a draft here because quiet off/on frame-time cost has not been measured**, despite the functional Longshot run below. The original https://github.com/NotRexed/Hookshot/issues/2292 was approved by Joey-Magnus. His latest review says both requested changes were settled: bound the table lookup and use the existing bridge instead of a parallel file channel. That approval predates the later age-command rebase and this move; it is not a new forge approval. ## What changed - `host/oot_state_tap.c` reads bounded guest values only in Link's update under the existing debug lock. Copied JSON crosses to the socket thread; it does not follow guest pointers there. - `status.game` carries 14 core fields, including sample frame/time, position, camera, life and the existing event/loading/age values. Unknown values are null. A retained sample during pause/loading is not fresh control. - Opt-in `oot` subscriptions have independent cadence, watched flags and change baselines. At most 64 watched flags and 32 reported changes; both lookup and scan reject tables larger than 2^22 slots. Pending samples are not overwritten before consumption. - Disconnect/replacement/unsubscribe clear the old watch. Protocol 1, ordinary state events and default subscriptions stay unchanged. The old file channel and its four switches are gone. - The renamed client exposes the snapshot and `watch --events oot --every 6 --flags ... --changes`. Main's typed age-command accessor stays intact; the JSON accessor is separately named `oot_sample`. - New local fixture adaptation: the two C fixture compiler commands define `_CRT_SECURE_NO_WARNINGS`, matching the repository's UTF-8 fixture convention. This applies the CRT declaration policy to Windows-target fixture compilation while retaining `-Werror` and every behavioral assertion. No native runtime adaptation was needed beyond the project rename. Compared with GitHub head f2970916a626dc9f7a3eb1079063881c1687da2d, all four host files, the client, `tests/control_test.c` and the client tests are byte-identical after the mechanical rename. Only `tests/test_control.py` and `tests/test_oot_state_tap.py` add that compiler definition; documentation adds current evidence. The historical fork checks passed the control/client checks on both systems, but the whole run had then-known unrelated failures; it was not globally green and is not new Windows evidence. **Overlap with another open pull request:** this and [pull request 47](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/47) (command-line test sessions) both change `scripts/longshot_ctl.py`, `tests/test_longshot_ctl.py` and the results table. Each merges cleanly into main on its own; merged together they conflict in those three files, so whichever goes second needs a rebase, which I will do. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_CONTROL` | Extended existing switch: Enable copied core sample and opt-in oot events; no guest reads when off | Unset/off | Off by default: debug/control channel | Linux synthetic-guest and Longshot forest session | Ordinary play without control | Fresh Windows, other GPUs; overhead (should help on: Controller-free tests on other platforms) | | `watch --events` | Extended: Accept oot event alongside existing event kinds | present,state; log alone selects log | Off by default: oot must be requested | Linux offline checks | Unrelated game/rendering behavior | Live services and Windows (should help on: Same protocol on Windows) | | `watch --every` | Extended application: Cadence of requested oot samples in game presents | 60 presents | Off by default: caller opts into denser samples | Linux offline checks | Unrelated game/rendering behavior | Live services and Windows (should help on: Same protocol on Windows) | | `watch --flags` | New: Up to 64 named or explicit-hash values in oot samples | Empty watch list | Off by default: bounded opt-in reads | Linux offline checks | Unrelated game/rendering behavior | Live services and Windows (should help on: Same protocol on Windows) | | `watch --changes` | New: Track Bool/Int/Enum/UInt changes per subscription | False | Off by default: scans have a cost | Linux offline checks | Unrelated game/rendering behavior | Live services and Windows (should help on: Same protocol on Windows) | | `tests --source-root` | New fixture selector: Select production source for negative-check comparisons | Test checkout | Off by default: test-only override | Linux offline checks | Unrelated game/rendering behavior | Live services and Windows (should help on: Same protocol on Windows) | | `test_oot_state_tap.py --cc` | New fixture selector: Choose the C fixture compiler | gcc | Off by default: platform testing override | Linux offline checks | Unrelated game/rendering behavior | Live services and Windows (should help on: Same protocol on Windows) | | `test_oot_state_tap.py --mutate` | New fixture selector: Inject a named regression into temporary source only | None | Off by default: negative-test mode | Linux offline checks | Unrelated game/rendering behavior | Live services and Windows (should help on: Same protocol on Windows) | | `-D_CRT_SECURE_NO_WARNINGS` | New test build flag: Declare portable CRT use before fixture includes; keep Werror | Defined for both fixture compile commands | On by default: portable fixture contract | Linux offline checks | Unrelated game/rendering behavior | Live services and Windows (should help on: Same protocol on Windows) | | `HOS_OOT_STATE_FILE` | Historical prototype only; not carried: Ignored: the file channel is replaced by the bridge | Unset and ineffective | Off by default: use the single bridge | Legacy-switch rejection fixtures | No file stream remains | Native Windows execution (should help on: No live behavior intended) | | `HOS_OOT_STATE_EVERY` | Historical prototype only; not carried: Ignored: the file channel is replaced by the bridge | Unset and ineffective | Off by default: use the single bridge | Legacy-switch rejection fixtures | No file stream remains | Native Windows execution (should help on: No live behavior intended) | | `HOS_OOT_STATE_FLAGS` | Historical prototype only; not carried: Ignored: the file channel is replaced by the bridge | Unset and ineffective | Off by default: use the single bridge | Legacy-switch rejection fixtures | No file stream remains | Native Windows execution (should help on: No live behavior intended) | | `HOS_OOT_STATE_CHANGES` | Historical prototype only; not carried: Ignored: the file channel is replaced by the bridge | Unset and ineffective | Off by default: use the single bridge | Legacy-switch rejection fixtures | No file stream remains | Native Windows execution (should help on: No live behavior intended) | ## Measurements <!-- branch-state --> Branch state: on main `d725b3631982ed0035ea3ac9cbe1677e9fad260c`; head `365b02c32d0f46e208e3475b0811bd0001c203c5`; whole game-free suite on this head: `511 passed, 1 failed, 20 skipped` (main `d725b3631982ed0035ea3ac9cbe1677e9fad260c`: `510 passed, 1 failed, 20 skipped`); the commits are unchanged by rebases apart from the results table. <!-- /branch-state --> The sole failure in both suites in the branch-state block is `tests/test_updater_asset_digest.py`: local .NET SDK 10.0.113 cannot resolve `Microsoft.AspNetCore.App.Ref (= 10.0.13)`, error `NU1100`. The historical suites below also include this failure. main: d4a69456c9de5131589af1fcf9a09adba4c403d1 (2026-11-12) this PR: play-harness @ c26c0d5f76b71c57d8054fc091dac7b76e49615f Measurement provenance: main @ d4a69456c9de5131589af1fcf9a09adba4c403d1. c26c0d5f76b71c57d8054fc091dac7b76e49615f, including the final documentation rows; native game build @ 4e63c765375ac7fde48102cbb2702e7ec7e7b9d4. Hardware: Core i9-13900K, 96 GB RAM, Arch Linux 7.2.15; RTX 4090 / NVIDIA 615.78.08 for the real game. Scene: synthetic-guest/real-control-bridge fixtures; separate copied-save Kokiri Forest run, child Link standing, windowless, muted, 1280×720 captures, frame generation off, deferred shader preparation. Method: same sampler driver before/after/before/after after the initial whole suite, fresh HOME, repository-default gcc. Median below is successful fixture completion, not speed. One ordinary shared Longshot session supplies runtime state and image evidence. No quiet off/on overhead measurement. How to reproduce: Run `python -I -B tests/test_oot_state_tap.py --source-root CHECKOUT` with pinned main and PR checkouts, interleaved twice. For gameplay use a disposable copied Kokiri Forest save with HOS_CONTROL enabled, wait for 30 fresh controllable frames, subscribe to oot every 6 presents, read samples, press B for 8 frames, capture and quit. Use game.frame to date buffered samples. Main d725b3631 adds only scripts, docs and tests, with no game-code change; the measured d4a69456 comparisons and pinned game evidence remain valid and were not repeated. Original results below retain their measured commits; the new suite is recorded in the branch-state block. Baseline: main @ d4a69456 | | main @ d4a69456 | this PR @ c26c0d5f | |---|---|---| | Pair 1: sampler fixture completes | 0; sampler absent | 1; complete fixture passes | | Pair 2: sampler fixture completes | 0; sampler absent | 1; complete fixture passes | | Median successful completion | 0 | 1 | | Whole game-free suite | 507 passed / 2 failed / 20 skipped | 509 passed / 1 failed / 20 skipped | | Windows-target fixture syntax | Not repeated on main | 4 translation units, 0 failures; not Windows execution | ```text Before 1 and 2 (checkout prefix redacted): FileNotFoundError: [Errno 2] No such file or directory: '<main checkout>/host/oot_state_tap.c' After 1 and 2: PASS: socket status, all typed flags, collision, nulls, 6/12/default-60 cadence, independent baselines PASS: invalid subscription arguments are atomic; name/hash/count bounds and changes:false PASS: replacement, non-OoT replacement, unsubscribe and disconnect clear pending samples and baselines PASS: 32-change cap + more, moved/resized table baselines, adult and unreadable/non-finite fields PASS: pending sample is retained, status stays current, and polling resumes without losing changes PASS: 64 longest names, maximally escaped strings, changes and a 199-byte id form complete socket JSON PASS: OoT socket sampler, preserved hash/constants/lock wiring, both table caps and no file switches Final whole suite: 509 passed, 1 failed, 20 skipped in 280 s (linux, 6 at once) PASS: 4 captured translation units, 0 failures, 0 unjudged drivers/files; Windows execution NOT TESTED ``` The before failure is feature absence, not a regression in main. The full suite ran on the **exact final head above**, after the last commit, with `--jobs 6 --strict`, fresh HOME and default compiler. Main's shared suite was run once for this day's moved pull requests on d4a69456. These checks have not yet been run on a forge runner. Main also failed two `tests/test_appimage_package_retry.py` timing/sequence cases under heavy load; its requested isolated rerun passed all 9 tests. It fails only under heavy load on this PC; timing-dependent. This branch passed it. Neither full suite is described as all green. The first Windows-target check reported missing CRT definitions for four translation units. After the separate fixture-only correction, all four passed. Native Windows execution of both the control and sampled-state fixtures remains untested. ### Real Longshot session Both native binaries were rebuilt from this branch, using a cloned immutable release-profile donor, with 0 failed compiles. Banner: `longshot v2.2.0-rc.1-26-g4e63c7653 (source 20775991)`. The standard-library session client started this game, waited for fresh controllable state sustained for 30 frames, read state, subscribed, pressed B, captured a picture and observed stop. Input came from a copied save; its input copy was unchanged. The 60.9 s run had load 33.94 → 51.29: not a performance comparison. Selected fields from the actual replies: ```json {"state":"play","frame":1499,"game":{"frame":1499,"event":false,"loading":false,"age":"child"}} {"event":"oot","game":{"frame":1500}} {"event":"oot","game":{"frame":1506}} {"ok":true,"frame":1514,"start":1506,"end":1514} {"event":"oot","game":{"frame":1512}} {"event":"oot","game":{"frame":1518}} {"ok":true,"frame":1640,"present":1639,"width":1280,"height":720} {"state":"finished","exit_code":0,"process_exited":true,"save_drain":"unsupported"} ``` All four samples carried all 14 core fields, at six-frame intervals. Sample 1512 was buffered during the press; 1518 is after its completion. Delivery order is not sample time. The inspected image shows Link, the forest, hearts and minimap. Fatal=0, crash=0, device-loss=0, kernel faults=0 and NVRM=0, with complete journal coverage. Save draining is unsupported. Historical Hookshot evidence, not repeated or relabelled: the 2026-11-11 file/socket comparison on base a5b955fb, builds d847c0cf/a57f35fe, found nine standing fields equal and a watched Bool transition also in `changed`. Both exited 0. The socket picture was HUD on black while shaders remained queued, so no rendering-parity claim was made. A later Hookshot f2970916 run exercised child/adult state and the age command. Those are older builds, not this new run. The complete suite was rerun after every source/documentation commit, with the default compiler and fresh HOME. The earlier measurement rows remain labelled by their actual commits. Main's suite was run once for all branches on this commit. Runner execution took 182 s on the branch, not a performance measurement. No hosted CI or new game run is claimed. ## Testing **Tested:** on this PC (Arch Linux, RTX 4090 with NVIDIA 615.78.08, i9-13900K), the current-head whole game-free suite and the earlier two interleaved main/branch sampler-fixture pairs at their measured commits above. The fixture covered socket status, typed flags, nulls, cadence, subscription bounds, independent baselines and cleanup; main's missing sampler was a feature-absence negative. Four translation units compiled for the Windows target; not run on Windows. In the Longshot Kokiri Forest run at native build `4e63c765`, waited for fresh controllable play for 30 frames, read state, subscribed every 6 presents, pressed B for 8 frames, checked sample frames, took a screenshot and observed the game exit. The source save copy stayed unchanged; save draining remains unsupported. Tested but not measured: The 60.9-second Longshot forest session and older Hookshot transport/age runs prove functionality, not sampling overhead, FPS or rendering parity. **Not tested:** the explicitly listed platforms, integration and runtime cases below remain untested; only the bounded Linux/NVIDIA forest session above was run for Longshot, under shared load. - Quiet on/off frame-time cost, sampling overhead, long runs, a complete playthrough, real controllers, AMD/Intel/Steam Deck or a real window. No hardware test request was posted during this move. - The new run did not exercise age changes, map changes, pause/resume or flag transitions; those have historical or game-free evidence, not a new Longshot claim. - When `HOS_CONTROL` is unset the source returns before reads or allocation; this source fact is not a measured zero-cost claim. With changes enabled, scanning still costs work despite the output cap. **Keep draft pending the feature template's off/on cost evidence.** **Needs other testers:** - Windows x64: build the host, run `tests/test_control.py` and `tests/test_oot_state_tap.py`, then subscribe to `oot` every 6 presents in controllable Kokiri Forest play and disconnect/reconnect, expecting passing fixtures, correctly dated samples and no stale watch after reconnect (request to open) ## Screenshots Nothing on screen is changed by the read-only sampler. main @ d4a69456 has no paired rendering capture; this PR @ 4e63c765 is the native build pictured below (runtime identical at c26c0d5f). No rendering-parity claim. ![New Longshot branch run, Kokiri Forest: Link and HUD visible at present 1639 after the sampled-state/press sequence. Not a before/after rendering comparison.](https://git.magicbean.dev/attachments/08db99ca-6ff2-4e1b-ba60-4f9ea196a396) ![Historical Hookshot file/socket evidence, 2026-11-11: left has Link; right is HUD on black with queued shaders. Its explicit rendering-parity limitation is retained.](https://git.magicbean.dev/attachments/41d3d605-ab5d-4f8d-8c3d-23eff7486095) ## Risk and rollback A stale sample can mislead an automated controller; callers must require fresh game.frame and known-false event/loading. changes=true scans tables on the guest update under a lock and has unmeasured cost. Unset HOS_CONTROL to disable the sampler, unsubscribe from oot to remove watch scans, or revert the commit. This does not write guest memory or promise save-safe shutdown. ## Reviewer notes Start with hos_oot_state_tap and its early return, range checks, table caps and state_lock handoff; then connection cleanup and pending-sample handling in control.c. Keep main's typed oot_state age callback distinct from oot_sample. Quiet off/on cost and native Windows execution remain unmeasured. The live description's PR47 overlap warning is retained; whichever lands second still needs integration. ## Issues Original pull request and reviews: https://github.com/NotRexed/Hookshot/issues/2292. Original tracking report: https://github.com/NotRexed/Hookshot/issues/2335, closed to consolidate discussion in that pull request. No corresponding issue exists in the forge's public issue inventory checked for this move. No old GitHub number is used as a forge closing reference. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. Functional pairs are retained; quiet off/on feature cost remains unmeasured. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). Relevant command/protocol details are in the topic guide; no unrelated menu default is added. - [x] A row in `docs/tested-configurations.md` for each measured run. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Linux game built; Windows fixture syntax only, not a fresh full host build. - [x] Docs updated. --- Moved from NotRexed/Hookshot#2292. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <[email protected]>
A script that plays the game through HOS_INPUT_LIVE sees only pictures and the log, so it cannot tell where Link
is or whether a cutscene holds him before its next press. With HOS_OOT_STATE_FILE=<file> the update of Link writes
a line of JSON every HOS_OOT_STATE_EVERY presents (6): the map, Link's position and facing, the camera's yaw,
whether an event plays, loading, age, life, rupees, the A button's action and the music, read from the same
singletons, GameDataMgr tables and actor fields the debug tools use. HOS_OOT_STATE_FLAGS adds flags by name and
HOS_OOT_STATE_CHANGES=1 every Bool, Int, Enum and UInt flag that changed. Nothing in the game is written; without
the switch nothing is read.

tests/test_oot_state_tap.py runs the whole file against a guest memory of its own and reads the lines back.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
state_entry probed up to `size` slots, and size is read from guest memory:
a torn or garbage read while a save loads could make one line walk
billions of slots on a guest worker. state_put_changes already refuses a
table that reports more than 1 << 22 slots; state_entry now does too
(review of #2292).

tests/test_oot_state_tap.py sets such a size on a table whose first slot
matches the flag asked for and expects no entry; with the limit itself
the flag is still found. It fails without the change, with gcc and clang.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Move read-only Link snapshots into status.game and an opt-in oot subscription. Sample guest memory only in the existing locked Link update, hand copied snapshots to the bridge, and keep watch lists and change baselines per connection. Drop the state-file switches and preserve the bounded table readers.

Carry the file sampler checks over to real socket/pipe fixtures, preserve existing bridge/client coverage, and add focused regression mutations.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep a changed flag in the same hash slot across the synthetic resize, so a missing fresh-baseline reset cannot pass merely because every key rehashed. Exercise the bridge existing 199-byte accepted and 200-byte rejected JSON ID boundaries without changing their implementation.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Record the exact field comparison, the watched-flag transition, the interrupted first analysis, and the HUD-only candidate capture without claiming rendering or frame-rate parity.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
On Windows longshot_ctl.py waited for a busy pipe only when the error
carried winerror 231. Python's open() goes through the C runtime, which
has no errno for ERROR_PIPE_BUSY: it raises "[Errno 22] Invalid
argument" with no winerror, so the wait never happened. With one client
it did not show. tests/test_oot_state_tap.py connects four clients in a
row, and the Windows checks failed there with exactly that error when
the bridge's next pipe instance was not up yet.

The client now waits, up to its timeout, for either form. A pipe that
does not exist (ENOENT) is still reported at once.

Tested: Linux, Python 3.12: three cases in tests/test_longshot_ctl.py
drive the Windows branch with open() stood in: busy in both forms then
connected, a missing pipe not waited for, a pipe that stays busy fails
at the deadline. With main's client the first case fails ("no game
listens ... Invalid argument"), the other two pass.
tests/test_longshot_ctl.py, tests/test_oot_state_tap.py and
tests/test_control.py pass as before.
Not tested: on Windows itself (the project's Windows job does that; it
is where the failure was seen).

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Match the UTF-8 fixture compiler contract by defining _CRT_SECURE_NO_WARNINGS before either translation unit includes CRT headers. The local llvm-mingw compatibility preflight rejects otherwise valid portable fopen/getenv/memcpy calls without this declaration. Keep -Werror and every behavioral assertion; no runtime code changes.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Author
First-time contributor

Discussion carried over from NotRexed/Hookshot#2292

Every comment that was readable on the original thread when it was copied on 2026-11-12, in order, unchanged except that issue and pull request numbers are written as links to the old repository and names carry no @. Comments by an account that has since been closed there could no longer be read and are missing; where a reply quotes them, the quote remains.


mooki000, 2026-11-11 17:08 UTC (comment, original):

Rebased onto main b29967bf so the workflows run here: they had waited for an approval on the old head, and main's game-free tests are green again since NotRexed/Hookshot#2380. The patch is unchanged (git range-diff: the commit is equal), and its test passes on the new base.


Joey-Magnus, 2026-11-11 20:07 UTC (review: approved, original):

Nice, self-contained addition. Some specifics I checked:

  • The file is new (host/oot_state_tap.c). nx2/build.py compiles every host/*.c, so nothing else needs wiring.
  • The hook in hos_oot_lift_player runs under the existing cheats_lock.
  • While HOS_OOT_STATE_FILE is unset, the cost is one getenv on the first call and then an early return.
  • Every guest read goes through hos_guest_range, and nothing in the game is written.
  • The camera path (0x84ca6bc0 → manager +32/+40 → camera +120) matches oot_gameplay_view in host/hos_main.c.
  • The switches are documented in docs/development.md.
  • fopen on a UTF-8 path is fine on Windows because of the exe's UTF-8 manifest; tests/test_utf8_paths.py passes.

tests/test_oot_state_tap.py passes here (gcc, -Werror). The red Windows check is tests/test_native_astc_texture_count.py, which main fixed in NotRexed/Hookshot#2397. The appimage failure was Launchpad timing out. Neither comes from this PR.

Minor points, none of them blocking:

  • minor host/oot_state_tap.c state_entry: the probe loop runs up to size iterations, where size comes from guest memory. state_put_changes caps it at 1 << 22, but this function doesn't. A torn or garbage read while a save loads could make one line walk billions of slots on a guest worker. Using the same cap here (if (size > (1u << 22)) return 0;) would keep a bad read from stalling the game.
  • question Fields that read the same thing: NotRexed/Hookshot#926's control socket status (now on main) and NotRexed/Hookshot#2048's !cheat 998 already report map, event, loading and age. You offered to move the fields into status. A maintainer should decide whether to keep two channels. I'm fine with this file as it is, because a file stream suits HOS_INPUT_LIVE scripts.
  • Not tested on Windows or AMD, as the description says. The code has nothing platform-specific beyond InterlockedCompareExchange64, which host/posix provides.

Joey-Magnus, 2026-11-11 21:25 UTC (review: comment, original):

The code is clean (opt-in, read-only, every guest read checked, and the test passes).

To answer the earlier question: it isn't integrated with the control bridge. host/control.c already has status (event, loading, age) and subscribe (map, state), and docs/control.md describes the bridge as the one connection for tools. Since you offered, moving these fields into status/subscribe seems better than a second channel.

One fix either way: state_entry (host/oot_state_tap.c:107) walks up to size slots read from guest memory with no upper limit. The sibling function caps it at 1u<<22, and this one should too.


mooki000, 2026-11-11 22:31 UTC (comment, original):

hckrman101 Joey-Magnus thanks both.

  • The cap is in (d847c0cf): state_entry returns no entry for a table that reports more than 1 << 22 slots, the same limit as state_put_changes. The test's fixture sets such a size on a table whose first slot matches the flag asked for and expects no entry; it fails without the line, with gcc and clang. Rebased onto a5b955fb, so the Windows job has NotRexed/Hookshot#2397's fix.
  • Control bridge: not integrated at this head, as Joey-Magnus found. I'll do what I offered: move the fields into status/subscribe and drop the file switches, so the bridge stays the one connection for tools (unless NotRexed wants the file kept). I'll push that here once a run in the game shows the same values over the socket as the file gives.

mooki000, 2026-11-12 00:07 UTC (comment, original):

hckrman101 Joey-Magnus the one-channel question, answered in code:

I moved the fields into the bridge in a57f35fe: status.game is the snapshot; subscribe with events:["oot"], every, flags and changes streams it. The four file switches and file writing are gone. Reads stay in Link's locked update; the socket thread receives copies. The lookup cap from d847c0cf remains, and d06e4956 strengthens the resize-baseline check.

On Linux/RTX 4090, the same copied save gave identical map, position, facing, camera, life/max life, rupees, prompt and music over file versus socket. A watched Bool changed 0→1 and the same hash appeared in changed. Both games exited cleanly; no driver faults. gcc/clang checks pass, with 38 behavioral regressions rejected by each.

Windows and AMD are not tested. Nor is rendering parity: the socket capture showed HUD on black with shaders still queued. The full suite was 437 passed, 1 failed, 20 skipped; the failure was test_bake_mimo.py's unreadable-.zs assertion, a test and a script this change does not touch, and rerun on its own it passes here and on the base's tree. Details and all three runs are in the updated description.

NotRexed if you would rather keep the file stream for HOS_INPUT_LIVE scripts, say so: the first two commits are still the file version as reviewed (d847c0cf), and the move is the three commits above them, so it can come out again or the file can stay beside the bridge.


mooki000, 2026-11-12 06:07 UTC (comment, original):

Rebased the actual pushed head 045fc13d onto current main 87991bc8 as 723758ff. The only conflict was additive test-evidence rows; both contributors' records remain verbatim, with no runtime change, dropped commit or altered author/message/trailer. All 27 selected checks passed with GCC and Clang on the first rebase; the sampler, bridge and client controls failed against main's missing feature. Its suite was 470 passed / 1 inherited background-input fixture warning / 20 skipped. The final rebase adds only upstream NotRexed/Hookshot#2671 and changes no PR file blob; the three feature checks, final-main negatives, NotRexed/Hookshot#2671's offline check and Windows-target syntax all ran again. The final PR gate passes. The suite was not repeated, and no new gameplay, native Windows or performance evidence is claimed. Sources: the NotRexed/Hookshot#2292 evidence and final-handoff ledger above.


Joey-Magnus, 2026-11-12 14:32 UTC (review: approved, original):

Both open points from the earlier round are settled. The state_entry cap is in, and the fields now go through status.game and an oot subscription instead of a second file channel, as hckrman101 and I asked. Guest reads still happen only in Link's update under cheats_lock. Only copied JSON crosses state_lock. Watch names are restricted to [A-Za-z0-9_.] or 0x plus 8 hex digits before they go into JSON. Every path that drops a connection clears its watch. I merged it into current main: test_oot_state_tap, test_control, test_hookshot_ctl, test_utf8_paths and test_env_on pass.

Minor points:

  • host/oot_state_tap.c:351-371: a subscription with changes:true scans up to four GameData tables while holding state_lock on the game's worker, so a status call waits for that scan. Fine for a debug channel.
  • status.game.event, loading and age can now be null. Before, they were always booleans or a string. docs/control.md says so, but scripts that test == false will notice.
  • NotRexed/Hookshot#2694 calls G.game->oot_state(&event, &loading, &child), which this PR replaces. Whichever lands second needs to adapt. NotRexed/Hookshot#2561 also moves docs/control.md.

mooki000, 2026-11-12 18:54 UTC (comment, original):

Rebased 45310b24 onto 86a9cda2; new head f2970916.
The merged age command keeps main’s typed oot_state; our JSON copy is now oot_sample. No wire-protocol change.
test_control.py, test_hookshot_ctl.py and test_oot_state_tap.py pass with GCC and Clang (6 invocations).
Whole source check: 497 passed, 3 failed, 20 skipped. Each failure also occurs on untouched pinned main; this is not an all-green result.
Failures: the one-variant cache fixture’s missing ZSTD declarations, the missing Skip opening README row, and this isolated environment’s .NET reference pack. The first two are fixed on main since; this head merges cleanly with it.
Built this head from source; one windowless Linux/RTX 4090 run used a copied save.
status showed child then adult; age adult --arrive 60 succeeded. The same watch delivered 2 checked samples before and 2 after the reload (49 events received overall).
The run exited normally with no fatal, crash, device-loss or kernel-fault line. No performance claim.
A post-switch screenshot was captured and inspected; it is not a rendering-parity comparison.
The earlier approval predates this rebase. A full run of the project's checks in my fork, Windows included, ran on this head: the control and client tests pass on both systems, and the only failing tests were the ones failing on main at that time (since fixed there by NotRexed/Hookshot#2861 and NotRexed/Hookshot#2883). It also found one real Windows problem, fixed in the last commit: the client now waits for a busy pipe that open() reports as EINVAL.
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra [email protected]

## Discussion carried over from NotRexed/Hookshot#2292 Every comment that was readable on [the original thread](https://github.com/NotRexed/Hookshot/pull/2292) when it was copied on 2026-11-12, in order, unchanged except that issue and pull request numbers are written as links to the old repository and names carry no @. Comments by an account that has since been closed there could no longer be read and are missing; where a reply quotes them, the quote remains. --- **mooki000**, 2026-11-11 17:08 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2292#issuecomment-6802217554)): Rebased onto main `b29967bf` so the workflows run here: they had waited for an approval on the old head, and main's game-free tests are green again since [NotRexed/Hookshot#2380](https://github.com/NotRexed/Hookshot/issues/2380). The patch is unchanged (`git range-diff`: the commit is equal), and its test passes on the new base. --- **Joey-Magnus**, 2026-11-11 20:07 UTC (review: approved, [original](https://github.com/NotRexed/Hookshot/pull/2292#pullrequestreview-5864769372)): Nice, self-contained addition. Some specifics I checked: - The file is new (`host/oot_state_tap.c`). `nx2/build.py` compiles every `host/*.c`, so nothing else needs wiring. - The hook in `hos_oot_lift_player` runs under the existing `cheats_lock`. - While `HOS_OOT_STATE_FILE` is unset, the cost is one `getenv` on the first call and then an early return. - Every guest read goes through `hos_guest_range`, and nothing in the game is written. - The camera path (`0x84ca6bc0` → manager +32/+40 → camera +120) matches `oot_gameplay_view` in `host/hos_main.c`. - The switches are documented in `docs/development.md`. - `fopen` on a UTF-8 path is fine on Windows because of the exe's UTF-8 manifest; `tests/test_utf8_paths.py` passes. `tests/test_oot_state_tap.py` passes here (gcc, `-Werror`). The red Windows check is `tests/test_native_astc_texture_count.py`, which main fixed in [NotRexed/Hookshot#2397](https://github.com/NotRexed/Hookshot/issues/2397). The `appimage` failure was Launchpad timing out. Neither comes from this PR. Minor points, none of them blocking: - **minor** `host/oot_state_tap.c` `state_entry`: the probe loop runs up to `size` iterations, where `size` comes from guest memory. `state_put_changes` caps it at `1 << 22`, but this function doesn't. A torn or garbage read while a save loads could make one line walk billions of slots on a guest worker. Using the same cap here (`if (size > (1u << 22)) return 0;`) would keep a bad read from stalling the game. - **question** Fields that read the same thing: [NotRexed/Hookshot#926](https://github.com/NotRexed/Hookshot/issues/926)'s control socket `status` (now on main) and [NotRexed/Hookshot#2048](https://github.com/NotRexed/Hookshot/issues/2048)'s `!cheat 998` already report map, event, loading and age. You offered to move the fields into `status`. A maintainer should decide whether to keep two channels. I'm fine with this file as it is, because a file stream suits `HOS_INPUT_LIVE` scripts. - Not tested on Windows or AMD, as the description says. The code has nothing platform-specific beyond `InterlockedCompareExchange64`, which `host/posix` provides. --- **Joey-Magnus**, 2026-11-11 21:25 UTC (review: comment, [original](https://github.com/NotRexed/Hookshot/pull/2292#pullrequestreview-5865491273)): The code is clean (opt-in, read-only, every guest read checked, and the test passes). To answer the earlier question: it isn't integrated with the control bridge. `host/control.c` already has `status` (event, loading, age) and `subscribe` (map, state), and `docs/control.md` describes the bridge as the one connection for tools. Since you offered, moving these fields into `status`/`subscribe` seems better than a second channel. One fix either way: `state_entry` (`host/oot_state_tap.c:107`) walks up to `size` slots read from guest memory with no upper limit. The sibling function caps it at `1u<<22`, and this one should too. --- **mooki000**, 2026-11-11 22:31 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2292#issuecomment-6808645105)): hckrman101 Joey-Magnus thanks both. - **The cap is in** (`d847c0cf`): `state_entry` returns no entry for a table that reports more than `1 << 22` slots, the same limit as `state_put_changes`. The test's fixture sets such a size on a table whose first slot matches the flag asked for and expects no entry; it fails without the line, with gcc and clang. Rebased onto `a5b955fb`, so the Windows job has [NotRexed/Hookshot#2397](https://github.com/NotRexed/Hookshot/issues/2397)'s fix. - **Control bridge:** not integrated at this head, as Joey-Magnus found. I'll do what I offered: move the fields into `status`/`subscribe` and drop the file switches, so the bridge stays the one connection for tools (unless NotRexed wants the file kept). I'll push that here once a run in the game shows the same values over the socket as the file gives. --- **mooki000**, 2026-11-12 00:07 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2292#issuecomment-6810335469)): hckrman101 Joey-Magnus the one-channel question, answered in code: I moved the fields into the bridge in a57f35fe: `status.game` is the snapshot; `subscribe` with `events:["oot"]`, `every`, `flags` and `changes` streams it. The four file switches and file writing are gone. Reads stay in Link's locked update; the socket thread receives copies. The lookup cap from d847c0cf remains, and d06e4956 strengthens the resize-baseline check. On Linux/RTX 4090, the same copied save gave identical map, position, facing, camera, life/max life, rupees, prompt and music over file versus socket. A watched Bool changed 0→1 and the same hash appeared in `changed`. Both games exited cleanly; no driver faults. gcc/clang checks pass, with 38 behavioral regressions rejected by each. Windows and AMD are not tested. Nor is rendering parity: the socket capture showed HUD on black with shaders still queued. The full suite was 437 passed, 1 failed, 20 skipped; the failure was `test_bake_mimo.py`'s unreadable-`.zs` assertion, a test and a script this change does not touch, and rerun on its own it passes here and on the base's tree. Details and all three runs are in the updated description. NotRexed if you would rather keep the file stream for `HOS_INPUT_LIVE` scripts, say so: the first two commits are still the file version as reviewed (d847c0cf), and the move is the three commits above them, so it can come out again or the file can stay beside the bridge. --- **mooki000**, 2026-11-12 06:07 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2292#issuecomment-6816118106)): Rebased the actual pushed head `045fc13d` onto current main `87991bc8` as `723758ff`. The only conflict was additive test-evidence rows; both contributors' records remain verbatim, with no runtime change, dropped commit or altered author/message/trailer. All 27 selected checks passed with GCC and Clang on the first rebase; the sampler, bridge and client controls failed against main's missing feature. Its suite was 470 passed / 1 inherited background-input fixture warning / 20 skipped. The final rebase adds only upstream [NotRexed/Hookshot#2671](https://github.com/NotRexed/Hookshot/issues/2671) and changes no PR file blob; the three feature checks, final-main negatives, [NotRexed/Hookshot#2671](https://github.com/NotRexed/Hookshot/issues/2671)'s offline check and Windows-target syntax all ran again. The final PR gate passes. The suite was not repeated, and no new gameplay, native Windows or performance evidence is claimed. Sources: the [NotRexed/Hookshot#2292](https://github.com/NotRexed/Hookshot/issues/2292) evidence and final-handoff ledger above. --- **Joey-Magnus**, 2026-11-12 14:32 UTC (review: approved, [original](https://github.com/NotRexed/Hookshot/pull/2292#pullrequestreview-5876207768)): Both open points from the earlier round are settled. The `state_entry` cap is in, and the fields now go through `status.game` and an `oot` subscription instead of a second file channel, as hckrman101 and I asked. Guest reads still happen only in Link's update under `cheats_lock`. Only copied JSON crosses `state_lock`. Watch names are restricted to `[A-Za-z0-9_.]` or `0x` plus 8 hex digits before they go into JSON. Every path that drops a connection clears its watch. I merged it into current main: `test_oot_state_tap`, `test_control`, `test_hookshot_ctl`, `test_utf8_paths` and `test_env_on` pass. Minor points: - `host/oot_state_tap.c:351-371`: a subscription with `changes:true` scans up to four GameData tables while holding `state_lock` on the game's worker, so a `status` call waits for that scan. Fine for a debug channel. - `status.game.event`, `loading` and `age` can now be `null`. Before, they were always booleans or a string. `docs/control.md` says so, but scripts that test `== false` will notice. - [NotRexed/Hookshot#2694](https://github.com/NotRexed/Hookshot/issues/2694) calls `G.game->oot_state(&event, &loading, &child)`, which this PR replaces. Whichever lands second needs to adapt. [NotRexed/Hookshot#2561](https://github.com/NotRexed/Hookshot/issues/2561) also moves `docs/control.md`. --- **mooki000**, 2026-11-12 18:54 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2292#issuecomment-6832956079)): Rebased `45310b24` onto `86a9cda2`; new head `f2970916`. The merged age command keeps main’s typed `oot_state`; our JSON copy is now `oot_sample`. No wire-protocol change. `test_control.py`, `test_hookshot_ctl.py` and `test_oot_state_tap.py` pass with GCC and Clang (6 invocations). Whole source check: 497 passed, 3 failed, 20 skipped. Each failure also occurs on untouched pinned main; this is not an all-green result. Failures: the one-variant cache fixture’s missing ZSTD declarations, the missing Skip opening README row, and this isolated environment’s .NET reference pack. The first two are fixed on main since; this head merges cleanly with it. Built this head from source; one windowless Linux/RTX 4090 run used a copied save. `status` showed child then adult; `age adult --arrive 60` succeeded. The same watch delivered 2 checked samples before and 2 after the reload (49 events received overall). The run exited normally with no fatal, crash, device-loss or kernel-fault line. No performance claim. A post-switch screenshot was captured and inspected; it is not a rendering-parity comparison. The earlier approval predates this rebase. A full run of the project's checks in my fork, Windows included, ran on this head: the control and client tests pass on both systems, and the only failing tests were the ones failing on main at that time (since fixed there by [NotRexed/Hookshot#2861](https://github.com/NotRexed/Hookshot/issues/2861) and [NotRexed/Hookshot#2883](https://github.com/NotRexed/Hookshot/issues/2883)). It also found one real Windows problem, fixed in the last commit: the client now waits for a busy pipe that `open()` reports as EINVAL. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <[email protected]>
Author
First-time contributor

Rebased onto main d725b3631 as 365b02c32d with all eight patches unchanged; its scripts/docs/tests-only changes do not invalidate the earlier functional measurements or Longshot run. The new-template description keeps both uploaded pictures and the pull request 47 overlap warning, and adds the control/default table, reproduction steps, risks and rollback. The whole suite on this exact head finished 511 passed, 1 failed, 20 skipped, with only the known local .NET reference-pack failure; the WIP description check passes, with the quiet off/on cost measurement still explicitly outstanding.

Rebased onto main d725b3631 as 365b02c32d0f46e208e3475b0811bd0001c203c5 with all eight patches unchanged; its scripts/docs/tests-only changes do not invalidate the earlier functional measurements or Longshot run. The new-template description keeps both uploaded pictures and the pull request 47 overlap warning, and adds the control/default table, reproduction steps, risks and rollback. The whole suite on this exact head finished 511 passed, 1 failed, 20 skipped, with only the known local .NET reference-pack failure; the WIP description check passes, with the quiet off/on cost measurement still explicitly outstanding.
mooki force-pushed play-harness from c26c0d5f76 to 365b02c32d
Some checks are pending
PR description / Description follows the template (pull_request_target) Waiting to run
2026-11-13 01:31:09 +00:00
Compare
Author
First-time contributor

Added the Testing section and updated its checklist. Added one branch base/suite block, consolidated duplicate results and simplified the supporting wording; everything else is unchanged.

Added the Testing section and updated its checklist. Added one branch base/suite block, consolidated duplicate results and simplified the supporting wording; everything else is unchanged.
Some checks are pending
PR description / Description follows the template (pull_request_target) Waiting to run
This pull request has changes conflicting with the target branch.
  • docs/tested-configurations.md
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'play-harness:mooki-play-harness'
git switch 'mooki-play-harness'
Sign in to join this conversation.
No description provided.