feat(gameplay): a Damage taken row (Half to Quadruple) #138
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!138
Loading…
Reference in a new issue
No description provided.
Delete branch "simonwjackson/Longshot:feat/damage-taken"
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?
Why
Players who struggle with combat, or want more challenge, can change how much health a hit removes without using invincibility.
Carried over from https://github.com/NotRexed/Hookshot/pull/3005
What changed
vk_menu.cpp/vk_menu.h, throughvk_replay.cpp, with Half / Normal / Double / Quadruple. Normal stays the default. The row follows the Debug tools visibility; hiding the tools does not reset or disable the saved damage setting.host/oot_damage.cuses quarter-heart arithmetic; Half rounds up, lethal damage saturates at zero, healing stays unchanged and Heart protection wins.patch_life()innx2/oot_patches.pyretains both player/life-pointer guards and is idempotent.test_oot_damage.pycovers arithmetic, guards, build gating, idempotence, wiring, default and Debug membership.test_damage_debug_menu.pycompiles the real Debug rows, visibility, group order and keyboard traversal. README and its menu-table test list Damage taken under Debug → Player.Flags and settings
damage_taken=0/1/2/3Measurements
main:
316f9319c8(2026-11-13 UTC)this PR: feat/damage-taken @
84fa59a120Hardware: AMD RX 7900 XT 20 GB, RADV Mesa 26.2.3; Ryzen 5 7600X, 32 GB RAM; NixOS 26.11, Linux 7.2.7. Private headless Sway 1.12/Xwayland; udmabuf enabled;
RX_VK_FORCE_MESH_FALLBACK=1; speakers disabled.Scene: Hyrule Castle Town (
Market_Child), stationary at the copied save's wooden door. 1920×1080 drawable and output, 100% render resolution, game's own upscaler, frame generation Off, game frame-rate cap Uncapped, VSync Auto, HDR Off. Effective menu values were read through the control bridge. No overlays, injectors or external shader-cache tools.Method: One short session, main → PR → main → PR, with fresh copies of the same save. Each process warms at the stationary scene for at least 10 s, then samples for 20 s; the last five
vk: N presents, X fpslog lines per run are retained. The per-run median and median of the two run medians are below. These are native-present measurements at the settings below, not a claim about other scenes or 1% lows.How to reproduce: Load a disposable copy of a Castle Town save and stay by the same wooden door, without movement/camera input. Use the settings above, with Damage taken Double on this PR; baseline main has stock behavior. Alternate the two binaries twice, let each settle, and take the last five presents-log readings per run. The additional feature proof below is separate from these stationary FPS samples.
316f931984fa59a1Damage proof: after the FPS samples, enable Debug flight on the disposable copy, get onto the nearby roof, rise about 19.4 m and land on the same roof. Stored player life was read through the read-only control bridge, in quarter hearts. A hardware breakpoint located the isolated player once and detached before the falls; no memory writes were used.
Half rounds Normal's three-quarter-heart hit up to two quarters. Double removes six quarters. Quadruple would remove twelve but saturates at zero with five left; no underflow. Heart protection was Off. Healing, Heart protection precedence and other damage sources were not freshly played; arithmetic/guard fixtures cover them and the external request asks for gameplay coverage.
At the original stationary spot, before any flight, the live Normal-setting sample was 53.10 FPS (last five), against 52.70 FPS for the immediately preceding Double sample on the same PR binary. This is one short observation, not a universal performance claim. The entire session took 460.2 s.
Normal-setting presents logs
Exact presents-log samples (five per run)
The binaries are the previously built Linux release copies at these exact commits, with all tracked source hashes rechecked against their Git trees before this session. Native shader preparation ran for each copy before measurement. No newer or unbuilt binary is substituted.
Review follow-up: head
7c80c7f472ac65c9e8434a896eb5c97493d62f3aadds one commit directly on the measured PR head84fa59a120cae5c2e80f99a7fa5690988b5464fa, without a rebase or force-push. At HumanBean's request, Damage taken moves to Debug → Player beside Heart protection. Option ID 55,damage_taken, Normal, the damage arithmetic and the hook are unchanged. The FPS and controlled-fall measurements above remain evidence at their original named commits; no new FPS comparison is claimed. The Debug screenshot below was captured at that follow-up head.Current-main merge: current head
decf471cd896cd7c9b63252d55ae74929cc812c0merges the Longshot main snapshot fetched for this run,dd02c6cca9582f5bb1b992d5c43268bf5d846c3f, into the existing PR branch with a merge commit and a normal push; no history rewrite. The earlier measurements and screenshots remain evidence only at their named commits, not a new performance comparison on this main. The merge was rebuilt and smoke-tested as described below. Main advanced tofdc2840bduring validation; a three-way merge check against that newer head is clean, but it is not included in the tested binary.Testing
Current merge validation (
decf471cd896, 2026-11-13): README keeps main’s text and the Damage taken row under Debug → Player, before Developer rows. Focused feature/option-ID/README/menu checks: 5 passed, 0 failed. Full suite on zao, with/tmp/longshot-carry/env.shand the shared suite lock: 505 passed, 3 failed, 16 skipped in 512 s (linux, 8 at once). All three failures reproduce on untouched maindd02c6cc:test_display_pointer.py(missingwin::rx_optionfixture definition),test_texture_staging_publication.py(missinglog_debugfixture declaration), and the known Nix-pathtest_linux_tools.pyfailure. No new failures.git diff --check dd02c6ccpasses for the feature diff; main itself contains an unrelated font-license trailing space. No standalone formatter/typechecker is configured; the suite compiles native fixtures and byte-compiles tracked Python. The pinned source was hash-audited before and after a successful Linux build on aka. The new build loaded a copied Castle Town save (Market_Child, stateplay) in a private headless Sway/Xwayland session on the RX 7900 XT/RADV; its runtime version and live options were read through the control bridge. Audio was disabled, the GPU lock held throughout, and both private units stopped. No owner game, live save orkorri*unit was touched. This was a short startup/gameplay smoke check, not a repeat of the earlier feature/performance measurements. Windows and other GPUs were not retested. The older test counts below describe the historical measured/follow-up heads, not this merge.Tested: fresh Linux main/PR gameplay and feature proof above. All screenshots linked here were opened and checked. Only isolated copies and copied test saves were used; no installed game, live save, owner game session or
korri*unit was changed. No GPU hang/device-lost/crash marker appeared in the completed measured runs.Review follow-up on zao: focused source/menu checks and
tests/test_option_ids.py: 18 passed, 0 failed, 0 skipped. This includes every test that extracts the menu table, plus tab membership, Debug group order and keyboard traversal. The new Debug membership assertion failed on the old PR head before the move.git diff --checkpasses. No standalone formatter/typechecker is configured; native fixtures compile with warnings as errors.Full suite:
source /tmp/longshot-carry/env.sh; flock /tmp/upstream-prs/suite.lock python -I -B tests/run_all.py --jobs 8 --timeout 1200: 517 passed, 1 failed, 20 skipped in 483 s. The only failure is the knowntests/test_linux_tools.pyfixture: it hardcodes/usr/bin:/bin, omitting Nix'sreadlink/dirname; the same failure is documented on untouched main.test_release.pypasses with OpenSSL supplied. One skip was a stale dav1d header path in the shared environment: rerunningtest_dav1d_av1.pywith the installed dav1d headers passed (1 passed, 0 failed, 0 skipped). The other 19 skips require Windows, game data or a Vulkan device. An initial run using onlymain-test-env.shhad the old HOOKSHOT header names and missing OpenSSL; it is superseded by this corrected full run, not counted as validation.Linux incremental build on aka succeeded in 7 s (only
vk_menu.cppand the host version stamp compiled; all other objects cached). All 2,144 tracked source hashes matched the committed tree before and after the build. In the isolated Castle Town copy on the RX 7900 XT/RADV 26.2.3, NixOS 26.11, opened F1 → Debug and selected Damage taken by keyboard: Normal appears directly after Heart protection under Player. The screenshot was opened and checked. No GPU-hang/device-lost/crash marker appeared; both private test units stopped and the GPU queue was cleared. No new damage/fall or performance measurement was needed for this menu-only follow-up.Not tested: Windows native builds/gameplay and non-ASCII Windows paths, physical controllers, other GPU families, HDR, full playthroughs and every feature combination. No Windows machine is available. Any combination specifically requested below remains unverified; a request is not a passing result.
Needs other testers:
These requests are open; if the service marks them waiting for a maintainer, they need the
testing wantedlabel before testers can claim them.Screenshots
Risk and rollback
Unknown damage sources may bypass the guarded life-write site. Choose Normal to restore stock damage; Heart protection still wins. Reverting the feature restores the old hook; reverting only the menu follow-up returns the row to Gameplay.
Reviewer notes
One feature only. The follow-up changes menu placement, tests and documentation, not damage behavior. Linux evidence does not certify Windows; that gap is assigned to open test requests rather than marked passed. No public MkwVkHost/MkwVkRenderer structure changed, so no ABI-version bump is needed. The shared tested-configurations file is unchanged; the measured runs are folded below. Authored glslang SPIR-V headers, where present, are source artifacts, not game-dump output.
Checklist
For everyone
type(scope): description).main. Merged the main snapshot fetched for this run,dd02c6cca958asdecf471cd896, as requested by the author; latest mainfdc2840bstill merges cleanly. No rebase or force-push.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.tests/that reads changed source updated in this PR, not weakened.Tested configurations: fresh Longshot runs, 2026-11-13 UTC
316f931984fa59a1316f931984fa59a1| Review follow-up | this PR @
7c80c7f4| RX 7900 XT, RADV 26.2.3; NixOS 26.11 / Linux 7.2.7 | Copied Castle Town save; 1920×1080; Debug tools On, Damage taken Normal | Keyboard navigation to Debug → Player; Damage taken directly below Heart protection. Screenshot inspected. Menu placement only; no new FPS sample. |Additional same-session feature/off-setting observations and their proof are described above.
This belongs more in the Debug/cheats
Added a fresh interleaved main/PR Linux measurement session at the named commits, the last-five-sample FPS medians, feature proof and inspected screenshots. Windows testing requests are open; the description names the remaining gaps. The branch also rebases cleanly onto main
1be180bbwith an identical stable patch ID and passing focused tests; the force-with-lease push is still gated, so the measured remote head stays pinned. No new baseline FPS is claimed for the local rebase.WIP: feat(gameplay): a Damage taken row (Half to Quadruple)to feat(gameplay): a Damage taken row (Half to Quadruple)Moved Damage taken to Debug → Player, beside Heart protection, as requested. Nothing else about the option changed: ID 55, damage_taken, Normal, damage behavior and hook are unchanged. Updated the README/menu tests and added a checked in-game Debug screenshot. Focused checks: 18 passed, including test_option_ids.py. Full suite under the lock: 517 passed, 1 known test_linux_tools.py failure, 20 skipped; the dav1d environment skip then passed separately, leaving 19 platform/game/hardware skips. Linux incremental build passed. One new commit,
7c80c7f472, pushed normally; no rebase or force-push.Merged main snapshot
dd02c6cca9with a merge commit and normal push; resolved README by keeping main’s text and Damage taken under Debug → Player. Five focused checks pass; full suite: 505 passed, 3 failures reproduced on untouched main, 16 skipped; Linux build and isolated Castle Town smoke pass, and newer mainfdc2840bstill merges cleanly (details in Testing).Thanks for this. It's a small, careful hook. Carried over from https://github.com/NotRexed/Hookshot/pull/3005.
What's good
f_804bd8c0instead of adding a new one. The guard keeps both checks: that the actor is the player and that the write goes to its own life pointer.patch_lifestill returnsNonewhen the head or the two CAS anchors are missing, soreport()warns "the player heart protection was found 0 times". It is now idempotent too, which the old guard wasn't. Changingnx2/oot_patches.pychangesmake_game.lift_key, so updating lifts the game again and nobody keeps a stale guard.next >= oldreturns it unscaled. A failed CAS loops back and recomputes the new value, so a retry is never scaled twice. The fairy or revive write is a third CAS that isn't guarded, which is correct because it's a gain. The game's own death or fairy check reads life back from memory after the write, so Double and Quadruple saturating at 0 still go through it.longshot_options.iniasdamage_taken, and the only thing written to game memory is the player's life, as the game itself does.patch_life's output and the realdebug_rowsanddebug_step. Nothing was weakened. Main has moved on tofbd42024since the mergeddd02c6cc, but none of its changes touch these hunks, so it merges cleanly.Must change
row_shownhides Damage taken when Debug tools is off, buthos_oot_damagekeeps applying the saved value. A player who picks Quadruple and then turns Debug tools off keeps taking four times the damage, and nothing on screen shows it or offers a way back except turning the tools on again. Every other tool on that tab stops when it's hidden: the Debug tools help says "Off puts flight, the free camera and heart protection away and Link back to his own speed". Since this was moved to Debug at the maintainer's request, make it behave like the tools there. The simplest fix is invk_replay.cpp:if (id == HOS_OPT_DAMAGE_TAKEN) return cheats_on() ? opts.damage_taken : 1;. The saved value is kept, so turning the tools on again restores it. Then update the README sentence ("Its saved setting still applies when Debug tools is off") and the help text, and flip thetest_damage_debug_menu.pyassertion to "hidden means Normal". The alternative is a row that is always visible, which would put it back under Gameplay, the placement that was declined.Should change
old - nextis the clamped damage. With 2 units left and a 4-unit hit, Half removes 1 and Link survives, where half of the real hit (2) would kill him. Double and Quadruple aren't affected, because they saturate to 0 anyway. At the damage site the unclamped amount is still in a register (x9) at the CAS. You could pass it through for that site, or document that Half can turn some lethal hits into survivals.hos_oot_is_playeronly checks the vtable and the "Player" resource name, so if Dark Link shares both, his damage is scaled too, and Heart protection would protect him as well); Ganon's final blows; drowning or cold water; heat-room or lava floors. The description already lists "every damage source" as untested. A test request for at least Dark Link would settle the main doubt.dd02c6cc, not a rebase. That's fine given the squash and the clean merge withfbd42024, but the box should say what was done.Reviewed by reading only: no build, no tests, no game run.
@ -963,2 +964,3 @@To enable the debug tools, turn **Debug tools** on in the menu's Debug tab (or add `HOS_OOT_CHEATS=1` to`longshot_env.txt` in the game folder). The tools then show below it on the same tab. The tab's Developer section,`longshot_env.txt` in the game folder). The tools then show below it on the same tab. **Damage taken** is in the **Player** group beside**Heart protection**: Half, Normal (the default), Double or Quadruple. Its saved setting still applies when Debug toolsThis sentence documents the hidden-but-active behaviour. If the effect is gated on Debug tools (see vk_replay.cpp), say instead that it applies only while the tools are on. Nit: the line above runs to 136 columns.
@ -1647,3 +1654,3 @@bool row_shown(int i) {const int* v = rows[i].value;return (rows[i].cheat < 0 || cheats_on()) && (v != &opts.reflex || reflex_available()) &&return ((rows[i].cheat < 0 && v != &opts.damage_taken) || cheats_on()) && (v != &opts.reflex || reflex_available()) &&Hidden with Debug tools off, but the host still applies it (see vk_replay.cpp). Either gate the effect the same way, or keep the row visible.
@ -5056,6 +5056,7 @@ void fill_replay_table(MkwVkRenderer& t) {if (id == HOS_OPT_CAMERA_SPEED) return opts.camera_speed;if (id == HOS_OPT_PROMPTS) return opts.prompts;if (id == HOS_OPT_FOV) return opts.fov;if (id == HOS_OPT_DAMAGE_TAKEN) return opts.damage_taken;This answers the saved value even while Debug tools is off and the row is hidden, so a hidden Quadruple (or Half) keeps applying with nothing on screen. Every other tool on that tab is put away by Debug tools Off. Suggest:
if (id == HOS_OPT_DAMAGE_TAKEN) return cheats_on() ? opts.damage_taken : 1;. The saved value survives, so turning the tools back on restores it.@ -0,0 +10,4 @@if (next >= old) return next;if (hos_oot_hearts_enabled()) return old;const int setting = rx_option(HOS_OPT_DAMAGE_TAKEN, 1);uint64_t damage = (uint64_t)old - next;old - nextis the damage after the game clamped life to 0, so on a lethal hit Half halves the clamped amount. For example, with 2 units left and a 4-unit hit, Half removes 1 and Link survives, where half the real hit (2) kills him. The unclamped damage is still in x9 at the damage-site CAS, if you want Half to be exact. Double and Quadruple aren't affected.I addressed my review in one commit on top of your merge commit (no rebase, so your history is unchanged). It's pushed as
b6d695b4and merges cleanly with current main.My commit, "Damage taken is Normal while the Debug tools are off":
menu_damage_taken()(ingpu/vk/vk_menu.cpp, declared invk_menu.h) returns the setting only while the Debug tools are on, and Normal otherwise. It's gated like Movement speed and the free camera throughcheats_on(), soHOS_OOT_CHEATS=1counts too.vk_replay.cppasks it forHOS_OPT_DAMAGE_TAKEN. The saved choice is kept and returns when the tools are turned on again.tests/test_oot_damage.pyandtests/test_damage_debug_menu.pynow assert the new behaviour: Normal while hidden, with the saved value untouched. They don't drop the old check.Not done: Half turning some lethal hits into survivals. The game clamps life at 0 before the hook sees the write, so Half halves the clamped damage: with 2 units left, a 4-unit hit leaves 1. I couldn't confirm from code that x9 holds the unclamped damage at the first CAS, including on the retry path. A wrong guess there would change every hit at every setting, so it needs someone with a lift and a run in the game. Until then, please note it in the help or the description as a known limit.
No tests or builds were run here.
Still yours:
hos_oot_is_playerchecks only the vtable and the "Player" name, so he may be scaled and protected too. Also Ganon's final hits, drowning and cold water, heat rooms and lava. Each with Half, Double and Quadruple, then Normal after turning the tools off with Quadruple saved.A heads-up from running the whole suite on main after this merged:
tests/test_menu_i18n.pyfails there (three runs on mainf3fe1763, and it passed onfbd42024before the merge).The Debug tools help text in
gpu/vk/vk_menu.cppnow ends "...Link back to his own speed and damage.", and the pt_BR entry ingpu/vk/menu_i18n.hstill has the old English sentence as its key. It needs the new sentence as the key and the Portuguese brought up to date. I am not opening a pull request for it, since the Portuguese wording belongs to whoever translates; until it is fixed I name this test as failing on main in my own pull requests.The merged change leaves a source-key mismatch in the menu translations: on unmodified main
f3fe1763, python -I -B tests/run_all.py test_menu_i18n.py fails MenuI18nTests.test_texts_are_the_menus. The first failure is kpt_BR retaining the previous Testing tools help text as its English lookup key while vk_menu.cpp now uses the extended help text. Please refresh the affected lookup keys/translations together; the test was not weakened. This was independently reproduced on native Windows/Python 3.14.7 while checking unrelated build-tool work.Thanks, monsterlag. The stale-key repair is isolated in #273: all 13 old Debug-tools translations omit the new damage-reset meaning, so the obsolete entries are removed and the complete English fallback stays. The unchanged translation checks now pass; the full suite is queued. The lethal-Half work remains separate in #269.
Thanks, HumanBean, for fixing the Debug-tools gate in
b6d695b4; I retained it unchanged.The remaining lethal-Half fix is in #269 (head
8c3c77c). The actual lift was checked on aka with first-try and forced-retry CAS fixtures; x9 preserves the full hit. Gameplay now proves 2 units left + a 4-unit hit at Half → 0 and death, with nonlethal Half, Normal and Double unchanged. Saved Quadruple also takes Normal damage with tools Off and restores Quadruple with tools On. The PR has the point-by-point method, public screenshot attachments, wrapped README and tested-configurations rows. Current main was merged, not rebased.Opened requests: T-ZJJT Dark Link, T-B8VV Ganon, T-53MS drowning/cold water, T-BHQP heat rooms/lava; each asks for Half/Double/Quadruple and Normal after tools Off with Quadruple saved. T-U5C9 covers Windows. They need the
testing wantedlabel. These paths remain untested; #269 stays WIP for the unmet Windows build box and the checker's literal rebase wording. The translation-key repair is separately ready in #273.