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
pull from: github/pr/2694
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2694
Loading…
Reference in a new issue
No description provided.
Delete branch "github/pr/2694"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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: anagecommand (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_agenow 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 instatus, 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.pyon Windows (llvm-mingw): 416 passed, 4 failed, 71 skipped. The 4 failures (fpx_sticky,bake_portability,input_background,lift_cache) fail onmaintoo on this PC (Python modules and a compiler warning here, not this change); CI runs them.test_controlonce: the test askedstatus5 frames after the switch, butstatusreports 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:
statusafterage adultage switch sent: the adult, reloading DeathMountainTrail_Adultadult, the mapDeathMountainTrail_Adultonce the reload is doneage adultagainLink is the adult alreadyage child5 s laterrefused: the game refused the age switch: another teleport is still in progress (one every 8 s)age --no-waitage switch to the child asked for: wait map to see it reloadchild, then the mapDeathMountainTrail_Childage childLink is the child alreadystatus'sagechanges at once; itsmapfollows when the reload ends.Not tested yet
python -I scripts/hookshot_ctl.py age adult, then (8 s later)age child, and post both reply lines.Overlaps
ageincluded, 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.docs/control.md, so this PR's section moves with it.🤖 Generated with Claude Code
In a dungeon the age switch reloads the same map name (
ForestTemple->ForestTemple, and so on for all 12 dungeon rows inoot_destinations), andwait mapwith amapis met immediately when the current map already matches, so this returns withwaited0 before the reload starts. A map event can't see it either, sincemap_changesonly counts a different name.Suggestion: when the reply's
mapequals the map before the switch (take it fromstatusfirst), waitloading_startthenloading_endinstead; or just always do that forage. A case intest_hookshot_ctl.pywith a dungeon map would pin it. The doc line aboutwait mapneeds the same caveat.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 | adultfromhos_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). Thentomeans what the docs say even when the sample is stale.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:
--arrivereturns 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, andwait mapwith amapis met as soon as the current map equals it. Soage adult --arrive 30inside a dungeon prints "in ForestTemple after 0 frames" before the reload has even started, and the doc's "wait mapthen tells when it arrived" is wrong there too. Details inline.The bridge asks for a toggle, not for an age.
cmd_agedecides fromoot_state's child flag, whichoot_map_refreshsamples 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 adultcan 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.3d8d02417dto34926f1dfeThanks, both fixed in
34926f1(branch rebased onto mainee29dd5too). CI is green.--arrivein dungeons: it now waits forloading_endinstead ofwait 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 isloading_endwith no map.hos_oot_age_ask(int adult)sendsOOT_BRIDGE_AGE | adult. When the player's update takes it, it readsSequence_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 noto, the direction still comes from the sample, but at worst that now ends in"sent": false, never a wrong flip.control_test.chas a stale case: the sample says child while Link is the adult, and bothage adultandage(noto) reply"sent": falseand 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):age adult --arrive 30age adultage toggle: Link is the adult already)age child --arrive 30I 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.
Fixed in
34926f1; details in the PR comment below.Fixed in
34926f1; details in the PR comment below.hckrman101's two points are both fixed in
34926f1.--arrivenow waits forloading_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 asOOT_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 staleoot_statesample can no longer turn an adult into a child. The control test covers that stale case. I merged this into current main and rantests/test_control.pyandtests/test_hookshot_ctl.pyon 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 inhost/control.c,host/control.handtests/control_test.c, so whichever merges second needs a rebase.34926f1dfetob30f4e7d75github-sync referenced this pull request2026-11-12 16:54:48 +00:00
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
34926f1don Arch Linux, kernel 7.2.15, i9-13900K, RTX 4090, NVIDIA 615.78.08. Built the exact head and baseba005ca4separately 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:
age adult --arrive 60age adultagainLink is the adult alreadyage child --arrive 60age adult --arrive 60age child --arrive 60Each 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:
--arrivewaits for loading to end, not for the arrival event to finish. On the base,age adultreturnsunknown_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.
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.