feat(control): the Debug tab's Switch age over the bridge, so scripts reach the adult's maps #2694

Closed
github-sync wants to merge 3 commits from github/pr/2694 into main
Owner

@nullifyshot opened this pull request on GitHub · 2026-11-12 06:37 UTC · original
Merged on GitHub in b157c3e817

Scripts and agents that drive the game through the control bridge can teleport, but they can't change Link's age. That leaves the adult's maps (and checks that need them, such as GPU numbers on Death Mountain Trail as an adult) out of reach unless there is a save of that age. The Debug tab's Switch age already does this in the game, but only from the Insert menu.

What changed

  • host/control.c: an age command (to: "child" or "adult"; leave it out for the other age). It is asked for and taken the same way a teleport is: at the player's next update in play, with the teleport's tickets, result and cancel. So the replies match too: sent, with the map it reloads; refused, with the game's reason; a timeout when not in play; cancelled when a later teleport or age switch replaces it. If Link is already that age, it says so and asks for nothing. Before a save is loaded, it is unavailable.
  • host/hos_main.c: hos_oot_age_ask. The player's update takes it as the menu's age action and reports through the bridge's teleport result. oot_toggle_age now says whether the map change was sent, and if not, why.
  • host/control.h: HosControlGame.age.
  • scripts/hookshot_ctl.py: age [child|adult] [--arrive SECS] [--no-wait].
  • docs/control.md: the command.

Tests

  • tests/control_test.c (the fake game): a bad argument, already that age, the switch and its map, the age in status, a timeout outside play, replaced by a later teleport, and without waiting.
  • tests/test_hookshot_ctl.py: the client's command.
  • python -I -B tests/run_all.py on Windows (llvm-mingw): 416 passed, 4 failed, 71 skipped. The 4 failures (fpx_sticky, bake_portability, input_background, lift_cache) fail on main too on this PC (Python modules and a compiler warning here, not this change); CI runs them.
  • The first CI run failed test_control once: the test asked status 5 frames after the switch, but status reports the game state the bridge samples every 100 ms. It now asks again until the new age shows (at most 2 s); 6 runs in a row pass under a full CPU load.

Tested in the game

RTX 5080, Windows 11. A build of this branch (with #2306's commits, which don't touch these files), headless, a child save in play on Death Mountain Trail:

Command Reply status after
age adult age switch sent: the adult, reloading DeathMountainTrail_Adult age adult, the map DeathMountainTrail_Adult once the reload is done
age adult again Link is the adult already unchanged
age child 5 s later refused: the game refused the age switch: another teleport is still in progress (one every 8 s) unchanged
age --no-wait age switch to the child asked for: wait map to see it reload age child, then the map DeathMountainTrail_Child
age child Link is the child already unchanged

status's age changes at once; its map follows when the reload ends.

Not tested yet

  • Linux: wanted. On any GPU, in play, run python -I scripts/hookshot_ctl.py age adult, then (8 s later) age child, and post both reply lines.

Overlaps

  • #2292 (mooki000) streams the game's state, age included, through the bridge, and touches the same files (host/control.c, host/control.h, scripts/hookshot_ctl.py, tests/control_test.c, docs/control.md). This PR doesn't stream anything, but whichever merges second will need a small rebase in those files.
  • #1025 (nicholasgower) can trigger Switch age from its MCP server through the menu's cheat action. That's a different interface, not this bridge command.
  • #2561 rewrites docs/control.md, so this PR's section moves with it.

🤖 Generated with Claude Code

> [@nullifyshot](https://github.com/nullifyshot) opened this pull request on GitHub · 2026-11-12 06:37 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694) > **Merged on GitHub** in https://git.magicbean.dev/MagicBeanDev/Hookshot/commit/b157c3e817666cf8848a85fa993311495a64f885 Scripts and agents that drive the game through the control bridge can teleport, but they can't change Link's age. That leaves the adult's maps (and checks that need them, such as GPU numbers on Death Mountain Trail as an adult) out of reach unless there is a save of that age. The Debug tab's Switch age already does this in the game, but only from the Insert menu. ## What changed - `host/control.c`: an `age` command (`to`: `"child"` or `"adult"`; leave it out for the other age). It is asked for and taken the same way a teleport is: at the player's next update in play, with the teleport's tickets, result and cancel. So the replies match too: sent, with the map it reloads; refused, with the game's reason; a timeout when not in play; cancelled when a later teleport or age switch replaces it. If Link is already that age, it says so and asks for nothing. Before a save is loaded, it is unavailable. - `host/hos_main.c`: `hos_oot_age_ask`. The player's update takes it as the menu's age action and reports through the bridge's teleport result. `oot_toggle_age` now says whether the map change was sent, and if not, why. - `host/control.h`: `HosControlGame.age`. - `scripts/hookshot_ctl.py`: `age [child|adult] [--arrive SECS] [--no-wait]`. - `docs/control.md`: the command. ## Tests - `tests/control_test.c` (the fake game): a bad argument, already that age, the switch and its map, the age in `status`, a timeout outside play, replaced by a later teleport, and without waiting. - `tests/test_hookshot_ctl.py`: the client's command. - `python -I -B tests/run_all.py` on Windows (llvm-mingw): 416 passed, 4 failed, 71 skipped. The 4 failures (`fpx_sticky`, `bake_portability`, `input_background`, `lift_cache`) fail on `main` too on this PC (Python modules and a compiler warning here, not this change); CI runs them. - The first CI run failed `test_control` once: the test asked `status` 5 frames after the switch, but `status` reports the game state the bridge samples every 100 ms. It now asks again until the new age shows (at most 2 s); 6 runs in a row pass under a full CPU load. ## Tested in the game RTX 5080, Windows 11. A build of this branch (with #2306's commits, which don't touch these files), headless, a child save in play on Death Mountain Trail: | Command | Reply | `status` after | |---|---|---| | `age adult` | `age switch sent: the adult, reloading DeathMountainTrail_Adult` | age `adult`, the map `DeathMountainTrail_Adult` once the reload is done | | `age adult` again | `Link is the adult already` | unchanged | | `age child` 5 s later | `refused: the game refused the age switch: another teleport is still in progress (one every 8 s)` | unchanged | | `age --no-wait` | `age switch to the child asked for: wait map to see it reload` | age `child`, then the map `DeathMountainTrail_Child` | | `age child` | `Link is the child already` | unchanged | `status`'s `age` changes at once; its `map` follows when the reload ends. ## Not tested yet - Linux: wanted. On any GPU, in play, run `python -I scripts/hookshot_ctl.py age adult`, then (8 s later) `age child`, and post both reply lines. ## Overlaps - #2292 (mooki000) streams the game's state, `age` included, through the bridge, and touches the same files (`host/control.c`, `host/control.h`, `scripts/hookshot_ctl.py`, `tests/control_test.c`, `docs/control.md`). This PR doesn't stream anything, but whichever merges second will need a small rebase in those files. - #1025 (nicholasgower) can trigger Switch age from its MCP server through the menu's cheat action. That's a different interface, not this bridge command. - #2561 rewrites `docs/control.md`, so this PR's section moves with it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- github-sync:item:2694 -->
A script or agent driving the game through the control bridge could
teleport but not change Link's age: the adult's maps (and tests that
need them, such as Death Mountain Trail at night as an adult) were out
of reach without a save of that age. The Debug tab's Switch age does it
in the game, but only from the Insert menu.

- host/control.c: an `age` command (to: "child" or "adult", none the
  other age). It is asked and taken as a teleport is, at the player's
  next update in play, with the teleport's tickets, result and cancel,
  so the replies are the same: sent with the map it reloads, refused
  with the game's reason, a timeout when not in play, cancelled when a
  later teleport or age switch replaces it. Already that age: said,
  nothing asked. Before a save is loaded: unavailable.
- host/hos_main.c: hos_oot_age_ask; the player's update takes it as
  the menu's age action and reports through the bridge's teleport
  result; oot_toggle_age now says whether the map change was sent, and
  why not.
- host/control.h: HosControlGame.age.
- scripts/hookshot_ctl.py: `age [child|adult] [--arrive SECS]
  [--no-wait]`.
- tests: the fake game's age switch in tests/control_test.c (bad
  argument, already that age, the switch and its map, status's age, a
  timeout not in play, replaced by a later teleport, without waiting);
  the client's command in tests/test_hookshot_ctl.py.
- docs/control.md: the command.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
status reports the game state the bridge's thread samples every 100 ms, so
5 frames of the fake game (about 10 ms) was often too soon on CI's runners.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Owner

@hckrman101 commented on scripts/hookshot_ctl.py line 323 on GitHub · 2026-11-12 09:16 UTC · original

In a dungeon the age switch reloads the same map name (ForestTemple -> ForestTemple, and so on for all 12 dungeon rows in oot_destinations), and wait map with a map is met immediately when the current map already matches, so this returns with waited 0 before the reload starts. A map event can't see it either, since map_changes only counts a different name.

Suggestion: when the reply's map equals the map before the switch (take it from status first), wait loading_start then loading_end instead; or just always do that for age. A case in test_hookshot_ctl.py with a dungeon map would pin it. The doc line about wait map needs the same caveat.

> [@hckrman101](https://github.com/hckrman101) commented on `scripts/hookshot_ctl.py` line 323 on GitHub · 2026-11-12 09:16 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694#discussion_r4548482307) In a dungeon the age switch reloads the same map name (`ForestTemple` -> `ForestTemple`, and so on for all 12 dungeon rows in `oot_destinations`), and `wait map` with a `map` is met immediately when the current map already matches, so this returns with `waited` 0 before the reload starts. A map event can't see it either, since `map_changes` only counts a different name. Suggestion: when the reply's `map` equals the map before the switch (take it from `status` first), wait `loading_start` then `loading_end` instead; or just always do that for `age`. A case in `test_hookshot_ctl.py` with a dungeon map would pin it. The doc line about `wait map` needs the same caveat. <!-- github-sync:rc:4548482307 -->
Author
Owner

@hckrman101 commented on host/hos_main.c line 1286 on GitHub · 2026-11-12 09:16 UTC · original

This runs oot_toggle_age, which flips whatever age Link has now, but the bridge chose the direction from a sample up to 250 ms old (longer through a load). I'd carry the wanted age in the ask, e.g. OOT_BRIDGE_AGE | adult from hos_oot_age_ask(int adult), and check it here at take time: if Link is already that age, finish the ticket without toggling (a refusal with why "Link is the adult already" is fine, or a separate result the bridge turns into "sent": false). Then to means what the docs say even when the sample is stale.

> [@hckrman101](https://github.com/hckrman101) commented on `host/hos_main.c` line 1286 on GitHub · 2026-11-12 09:16 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694#discussion_r4548482325) This runs `oot_toggle_age`, which flips whatever age Link has now, but the bridge chose the direction from a sample up to 250 ms old (longer through a load). I'd carry the wanted age in the ask, e.g. `OOT_BRIDGE_AGE | adult` from `hos_oot_age_ask(int adult)`, and check it here at take time: if Link is already that age, finish the ticket without toggling (a refusal with why "Link is the adult already" is fine, or a separate result the bridge turns into `"sent": false`). Then `to` means what the docs say even when the sample is stale. <!-- github-sync:rc:4548482325 -->
Author
Owner

@hckrman101 reviewed on GitHub · 2026-11-12 09:16 UTC · original

Thanks, this is a useful addition, and it's done the right way: it reuses the teleport's tickets/result/cancel instead of adding a second path, the fake game covers the replies (bad argument, already that age, timeout, replaced, no-wait), and the in-game table on the RTX 5080 shows the real replies including the 8 s refusal. CI is green. Nothing here is a performance or default change, so the in-game check you posted is what I'd want; a Linux run would still be nice but the code is shared, so I won't hold the PR on it.

Two things I'd like fixed before merging, both small:

  1. --arrive returns at once in every dungeon. 12 of the 31 destinations (Deku Tree through Inside Ganon's Castle) have the same map name for both ages, and wait map with a map is met as soon as the current map equals it. So age adult --arrive 30 inside a dungeon prints "in ForestTemple after 0 frames" before the reload has even started, and the doc's "wait map then tells when it arrived" is wrong there too. Details inline.

  2. The bridge asks for a toggle, not for an age. cmd_age decides from oot_state's child flag, which oot_map_refresh samples every 250 ms on the player's update (and not at all while Link isn't updated, e.g. through a load), and then the game side flips whatever age Link has when it takes the ask. If the sample is stale, age adult can turn an adult Link into the child while replying "age":"adult","sent":true. Most of the time the 8 s cooldown refuses it instead, but a long load or a menu Switch age between ask and take gets through. Details inline; it's a few lines.

Smaller: the "sent": false ("already that age") answer has the same staleness, so it would also be good to decide it at take time with the fix above. And as you say, #2292 touches the same files; whichever lands second rebases.

> [@hckrman101](https://github.com/hckrman101) reviewed on GitHub · 2026-11-12 09:16 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694#pullrequestreview-5871928154) Thanks, this is a useful addition, and it's done the right way: it reuses the teleport's tickets/result/cancel instead of adding a second path, the fake game covers the replies (bad argument, already that age, timeout, replaced, no-wait), and the in-game table on the RTX 5080 shows the real replies including the 8 s refusal. CI is green. Nothing here is a performance or default change, so the in-game check you posted is what I'd want; a Linux run would still be nice but the code is shared, so I won't hold the PR on it. Two things I'd like fixed before merging, both small: 1. **`--arrive` returns at once in every dungeon.** 12 of the 31 destinations (Deku Tree through Inside Ganon's Castle) have the same map name for both ages, and `wait map` with a `map` is met as soon as the current map equals it. So `age adult --arrive 30` inside a dungeon prints "in ForestTemple after 0 frames" before the reload has even started, and the doc's "`wait map` then tells when it arrived" is wrong there too. Details inline. 2. **The bridge asks for a toggle, not for an age.** `cmd_age` decides from `oot_state`'s child flag, which `oot_map_refresh` samples every 250 ms on the player's update (and not at all while Link isn't updated, e.g. through a load), and then the game side flips whatever age Link has when it takes the ask. If the sample is stale, `age adult` can turn an adult Link into the child while replying `"age":"adult","sent":true`. Most of the time the 8 s cooldown refuses it instead, but a long load or a menu Switch age between ask and take gets through. Details inline; it's a few lines. Smaller: the `"sent": false` ("already that age") answer has the same staleness, so it would also be good to decide it at take time with the fix above. And as you say, #2292 touches the same files; whichever lands second rebases. <!-- github-sync:r:5871928154 -->
Author
Owner

@nullifyshot commented on GitHub · 2026-11-12 09:50 UTC · original

Thanks, both fixed in 34926f1 (branch rebased onto main ee29dd5 too). CI is green.

  1. --arrive in dungeons: it now waits for loading_end instead of wait map. That count starts at the ask, so a reload that is already loading when the wait is asked still gets waited out. The doc says the same, and the client test checks that the wait is loading_end with no map.
  2. The age is checked when the game takes the ask: hos_oot_age_ask(int adult) sends OOT_BRIDGE_AGE | adult. When the player's update takes it, it reads Sequence_IsChildhood, and if Link is already that age it finishes the ticket with a new result 2 (nothing toggled), which the bridge turns into "sent": false. This replaces the bridge's own "already" check, so that answer no longer depends on a stale sample either. With no to, the direction still comes from the sample, but at worst that now ends in "sent": false, never a wrong flip. control_test.c has a stale case: the sample says child while Link is the adult, and both age adult and age (no to) reply "sent": false and leave him adult.

In game on a build of 34926f1 (RTX 5080, Windows 11), Inside the Deku Tree (the map has the same name for both ages):

Step Reply
age adult --arrive 30 in InsideTheDekuTree after 325 frames; status: adult, loading false
age adult Link is the adult already (the game's log: age toggle: Link is the adult already)
age child --arrive 30 in InsideTheDekuTree after 271 frames; status: child, loading false

I did not run the old build in a dungeon, so the "0 frames before" is from your reading, not a run here. Full suite: 418 passed; the 4 failures are main's own on this PC.

> [@nullifyshot](https://github.com/nullifyshot) commented on GitHub · 2026-11-12 09:50 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694#issuecomment-6820322273) Thanks, both fixed in 34926f1 (branch rebased onto main ee29dd5 too). CI is green. 1. **`--arrive` in dungeons**: it now waits for `loading_end` instead of `wait map`. That count starts at the ask, so a reload that is already loading when the wait is asked still gets waited out. The doc says the same, and the client test checks that the wait is `loading_end` with no map. 2. **The age is checked when the game takes the ask**: `hos_oot_age_ask(int adult)` sends `OOT_BRIDGE_AGE | adult`. When the player's update takes it, it reads `Sequence_IsChildhood`, and if Link is already that age it finishes the ticket with a new result 2 (nothing toggled), which the bridge turns into `"sent": false`. This replaces the bridge's own "already" check, so that answer no longer depends on a stale sample either. With no `to`, the direction still comes from the sample, but at worst that now ends in `"sent": false`, never a wrong flip. `control_test.c` has a stale case: the sample says child while Link is the adult, and both `age adult` and `age` (no `to`) reply `"sent": false` and leave him adult. In game on a build of 34926f1 (RTX 5080, Windows 11), Inside the Deku Tree (the map has the same name for both ages): | Step | Reply | |---|---| | `age adult --arrive 30` | in InsideTheDekuTree after 325 frames; status: adult, loading false | | `age adult` | Link is the adult already (the game's log: `age toggle: Link is the adult already`) | | `age child --arrive 30` | in InsideTheDekuTree after 271 frames; status: child, loading false | I did not run the old build in a dungeon, so the "0 frames before" is from your reading, not a run here. Full suite: 418 passed; the 4 failures are main's own on this PC. <!-- github-sync:c:6820322273 -->
Author
Owner

@nullifyshot commented on scripts/hookshot_ctl.py line 323 on GitHub · 2026-11-12 09:50 UTC · original

Fixed in 34926f1; details in the PR comment below.

> [@nullifyshot](https://github.com/nullifyshot) commented on `scripts/hookshot_ctl.py` line 323 on GitHub · 2026-11-12 09:50 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694#discussion_r4548807164) Fixed in 34926f1; details in the PR comment below. <!-- github-sync:rc:4548807164 -->
Author
Owner

@nullifyshot commented on host/hos_main.c line 1286 on GitHub · 2026-11-12 09:50 UTC · original

Fixed in 34926f1; details in the PR comment below.

> [@nullifyshot](https://github.com/nullifyshot) commented on `host/hos_main.c` line 1286 on GitHub · 2026-11-12 09:50 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694#discussion_r4548807473) Fixed in 34926f1; details in the PR comment below. <!-- github-sync:rc:4548807473 -->
Author
Owner

@Joey-Magnus approved on GitHub · 2026-11-12 14:34 UTC · original

hckrman101's two points are both fixed in 34926f1. --arrive now waits for loading_end (scripts/hookshot_ctl.py:325), so dungeons, where both ages share a map name, no longer return before the reload. The wanted age goes with the ask as OOT_BRIDGE_AGE | adult. It is checked against Link's live age when the player's update takes it (host/hos_main.c:1306), which finishes the ticket with result 2 and makes no flip, so a stale oot_state sample can no longer turn an adult into a child. The control test covers that stale case. I merged this into current main and ran tests/test_control.py and tests/test_hookshot_ctl.py on Linux: both pass. That covers the shared code path you asked about, though not an in-game Linux run. OOT_BRIDGE_AGE (0x10000) can't collide with a teleport index, and a later teleport replaces a pending age ask through the same ticket, as the reply text says. One heads-up: #2292 conflicts in host/control.c, host/control.h and tests/control_test.c, so whichever merges second needs a rebase.

> [@Joey-Magnus](https://github.com/Joey-Magnus) approved on GitHub · 2026-11-12 14:34 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694#pullrequestreview-5876235103) hckrman101's two points are both fixed in 34926f1. `--arrive` now waits for `loading_end` (`scripts/hookshot_ctl.py:325`), so dungeons, where both ages share a map name, no longer return before the reload. The wanted age goes with the ask as `OOT_BRIDGE_AGE | adult`. It is checked against Link's live age when the player's update takes it (`host/hos_main.c:1306`), which finishes the ticket with result 2 and makes no flip, so a stale `oot_state` sample can no longer turn an adult into a child. The control test covers that stale case. I merged this into current main and ran `tests/test_control.py` and `tests/test_hookshot_ctl.py` on Linux: both pass. That covers the shared code path you asked about, though not an in-game Linux run. `OOT_BRIDGE_AGE` (0x10000) can't collide with a teleport index, and a later teleport replaces a pending age ask through the same ticket, as the reply text says. One heads-up: #2292 conflicts in `host/control.c`, `host/control.h` and `tests/control_test.c`, so whichever merges second needs a rebase. <!-- github-sync:r:5876235103 -->
github-sync closed this pull request 2026-11-12 16:55:44 +00:00
Author
Owner

@mooki000 commented on GitHub · 2026-11-12 16:43 UTC · original

The head is now b30f4e7d: the same three commits rebased onto newer main (a range-diff marks all three identical, and the pull request's own files do not differ between the two heads); I did not rebuild for the rebase.

Tested 34926f1d on Arch Linux, kernel 7.2.15, i9-13900K, RTX 4090, NVIDIA 615.78.08. Built the exact head and base ba005ca4 separately with Clang 23.1.3. Windowless, copied save, 2048×1152 game output, 1920×1080 offscreen presentation, 60 cap, frame generation and DLSS off.

The Linux age commands work, including the same-map dungeon reload:

Place Command Reply's wait
Kokiri Forest age adult --arrive 60 403 frames
Kokiri Forest age adult again Link is the adult already
Kokiri Forest age child --arrive 60 275 frames
Inside the Deku Tree age adult --arrive 60 277 frames
Inside the Deku Tree age child --arrive 60 304 frames

Each switch eventually reported the requested age and map with loading/event false, and the inspected pictures show the adult and child models. I waited for that quiet state before taking the pictures: --arrive waits for loading to end, not for the arrival event to finish. On the base, age adult returns unknown_cmd: no command age.

Both games exited 0; the head ran 136.6 seconds and produced 4,947 game-present rows, the base 40.2 seconds and 1,516 rows. No crash, lost device or kernel driver fault. These were ordinary runs with other work active (load averages 52.09→48.46 for the head, 57.71→54.02 for the base), not performance measurements. Counts and reply rows were extracted and asserted by script.

Game-free: bridge and client tests pass with GCC 16.2 and Clang 23.1. The head's client test fails against base because the command is absent; the changed C fixture cannot compile there because the callback is absent. Full suite: 473 passed, 0 failed, 20 skipped. The changed C fixture also passes a Windows-target syntax check with its own warning/error flags; Windows execution was not tested here.

Child before switching, adult after the bridge's age command, adult after the same-map dungeon reload, child after the return

Not tested: other GPUs/OSes, all destinations, physical input, long sessions, or an intentionally stale real-game age sample. The stale-sample case is covered by the game-free fixture, not recreated in this game run. No new defect found in the exercised paths.

> [@mooki000](https://github.com/mooki000) commented on GitHub · 2026-11-12 16:43 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2694#issuecomment-6829817396) The head is now `b30f4e7d`: the same three commits rebased onto newer main (a range-diff marks all three identical, and the pull request's own files do not differ between the two heads); I did not rebuild for the rebase. Tested `34926f1d` on Arch Linux, kernel 7.2.15, i9-13900K, RTX 4090, NVIDIA 615.78.08. Built the exact head and base `ba005ca4` separately with Clang 23.1.3. Windowless, copied save, 2048×1152 game output, 1920×1080 offscreen presentation, 60 cap, frame generation and DLSS off. The Linux age commands work, including the same-map dungeon reload: | Place | Command | Reply's wait | |---|---|---:| | Kokiri Forest | `age adult --arrive 60` | 403 frames | | Kokiri Forest | `age adult` again | `Link is the adult already` | | Kokiri Forest | `age child --arrive 60` | 275 frames | | Inside the Deku Tree | `age adult --arrive 60` | 277 frames | | Inside the Deku Tree | `age child --arrive 60` | 304 frames | Each switch eventually reported the requested age and map with loading/event false, and the inspected pictures show the adult and child models. I waited for that quiet state before taking the pictures: `--arrive` waits for loading to end, not for the arrival event to finish. On the base, `age adult` returns `unknown_cmd: no command age`. Both games exited 0; the head ran 136.6 seconds and produced 4,947 game-present rows, the base 40.2 seconds and 1,516 rows. No crash, lost device or kernel driver fault. These were ordinary runs with other work active (load averages 52.09→48.46 for the head, 57.71→54.02 for the base), not performance measurements. Counts and reply rows were extracted and asserted by script. Game-free: bridge and client tests pass with GCC 16.2 and Clang 23.1. The head's client test fails against base because the command is absent; the changed C fixture cannot compile there because the callback is absent. Full suite: **473 passed, 0 failed, 20 skipped**. The changed C fixture also passes a Windows-target syntax check with its own warning/error flags; Windows execution was not tested here. ![Child before switching, adult after the bridge's age command, adult after the same-map dungeon reload, child after the return](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/67d20c0bce7f3135e063d3f1c0a6651784d50843/hookshot-pr2694-linux-ages.jpg) Not tested: other GPUs/OSes, all destinations, physical input, long sessions, or an intentionally stale real-game age sample. The stale-sample case is covered by the game-free fixture, not recreated in this game run. No new defect found in the exercised paths. <!-- github-sync:c:6829817396 -->
Commenting is not possible because the repository is archived.
No description provided.