native: thread a hit's own target through the same-tic fold - #513
Merged
Conversation
MarcusKainth
force-pushed
the
native/kills-and-drops
branch
2 times, most recently
from
September 7, 2026 16:55
eee99a6 to
6898aca
Compare
MarcusKainth
force-pushed
the
native/same-target-threading
branch
from
September 7, 2026 17:52
1852d9b to
8bc1005
Compare
damage_fold's own mobj fields (health, flags, state, tics, momentum, reactiontime, target, threshold) read the tic-start arrays directly rather than an accumulator, so a second hit on a target already hit this tic overwrote the first's own answer instead of building on it. DM_SAME_TARGET refused the tic for this rather than committing the wrong shared answer. The hurt tuple now carries HIT_RESULTS alongside HIT_TARGETS: each target's own latest mobj fields, in the same order HIT_TARGETS names the targets. A call reads a repeat target's own carried answer instead of the tic-start arrays, the same way the player's own fields already thread. DM_SAME_TARGET is gone, along with the field that only ever fed it. Threading exposed a second bug the refusal had been masking: the scatter that applies an in-flight missile's own hit back onto the target's mobj columns picked the first missile in the list whose own impact reached a slot, not the last. Two missiles in one list landing on the same target now read the list in reverse to find the one carrying the combined answer, the same fix in both removed's own missile scatter and thrown_thinks's own tk_hurt_at.
seed::row kept only the first override it found for a column name, so a caller wanting two different slots' own values in one column, and calling put() twice for it the way one arm here did, lost the second one silently and seeded the wrong row. The next arm that does this now fails loudly at the point it happens instead of testing data nobody asked for. sim_justattacked_live.rs's own two-slot m_x/m_y arm hit exactly this: the target's own position never actually moved to where the comment says it stands, north-east of the imp past CHASE_SLOP, since the real thing at slot 1 kept its own gametic-40 position instead. Combined into one put2 call each, matching the pattern sim_missile_kill_drop_live.rs already uses for the same shape.
The two-fireballs-on-the-player arm already proves the threaded fold for the player's own case, seeded from a real probe row. This proves the general one: two hand-seeded fireballs, on parallel lines that never touch in flight, landing on a third thing turned into a zombieman in the same tic. Both explode, the target's own health carries both hits, and the tic resolves.
missile::draws answered the impact's own damage roll plus P_DamageMobj's draws, and never reserved P_ExplodeMissile's own tics -= P_Random()&3, which the real thinker draws wherever the walk sets the missile off, whether or not what it stopped on was something this damages. now_prndindex and every base after an exploding missile were one short from the point the first one in a tic connects. The worst case now reserves it whenever the touch blocked the move, over the sky hack this does not model, which only ever draws less: the same shape as the existing fall check's own worst case, an over-reservation is safe where an under-reservation is the bug.
move_ceilingline forces the ceiling line to none whenever a thing blocked the move, since P_CheckPosition reaches things before lines and stops at the first that answers no. The sky hack reads that same line, so it can never take a missile a touch stopped - only one a wall or a special line blocked instead, which missile_draws does not reach at all. Reserving the explosion's own draw wherever a touch blocks is therefore the real count, not an over-reservation that happens to be safe: unlike a refuse bit, a wrong draw count would shift every later base silently rather than being caught.
missile_draws only reserved P_ExplodeMissile's own draw where a thing blocked the move, but a wall or a special line stops a missile exactly as a thing does, and misses the target far more often than a hit lands, so a wall-blocked missile is the ordinary case, not an edge one. mkd_xy_blocked now reads the same three thought_of's own mn_xy_blocked does - a thing, a step or ceiling too tight, and a special line - and the sky hack, which only a wall or a line can ever reach, is read for directly rather than assumed away. Seeded from demo3's own recorded run: the fireball at slot 265 explodes on a wall at gametic 224 with nothing on the level's own list taking damage, so PIT_CheckThing is not what stopped it. The whole tic's own prndindex agrees with the reference exactly, which a wall reserving nothing would have shifted along with every base after it.
MarcusKainth
force-pushed
the
native/same-target-threading
branch
from
September 7, 2026 17:58
8bc1005 to
92665b6
Compare
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes, and why
damage_fold's own mobj fields - health, flags, state, tics, momentum,reactiontime, target and threshold - read the tic-start arrays directly
rather than an accumulator. A second hit on a target this tic has
already hit once, whether two monsters' own shots on the player or a
shot and a claw, overwrote the first's own answer instead of building
on it, and
DM_SAME_TARGETrefused the tic for this rather thancommitting the wrong shared answer. Two hitscan monsters firing in the
same tic, which is ordinary once the attacker cap lifts, would hit this
on every such tic.
inter::hurtgainsHIT_RESULTS, an array parallel to the existingHIT_TARGETS: each target's own latest mobj fields, inHEALTHthrough
THRESHOLD's own order.reachanddamagedread a repeattarget's own carried answer from it instead of the tic-start arrays,
the same threading the player's own fields already ride.
hurt::SAME_TARGETandunresolved::DM_SAME_TARGETare gone, alongwith the refusal that only ever fed them.
Threading exposed a second bug the refusal had been masking. The
scatter that applies an in-flight missile's own hit back onto the
target's mobj columns (
removed's own missile scatter, andthrown_thinks's owntk_hurt_at) picked the first missile in thelist whose own impact reached a slot,
indexOf's own answer, not thelast. Two hits on the same target in one list now read the list in
reverse to find the one carrying the combined answer.
Evidence
Driver unit tests, clippy and formatting, on the final change:
Live, against a throwaway container:
two_fireballs_in_one_tic_thread_the_player_through_bothis the armthis PR's own fix reaches: two fireballs landing on the player in the
same
mt_missile_thoughtslist, both seeded from the real gametic 205probe row (
native/tests/fixtures/demo3-player-damage.tsv) plus arepurposed corpse on the same heading. Before this change it asserted
the gap directly - the shared
m_healthfield disagreeing with theplayer's own
p_health, andDM_SAME_TARGETset. It now asserts thetwo agree and the tic resolves, which is the one place the scatter fix
also had to land: without it, the shared field carried the first
hit's own answer rather than both, agreeing with neither the old
broken value nor the new correct one, and the arm still failed. The
other three live suites are unaffected call sites, rerun to confirm the
threading change carries nothing else's own behaviour with it.
Cherry-picked onto
origin/mainafter retargeting the PR's base:native/kills-and-dropsmerged as #509, so this branch's own sixcommits (
c422c50through1852d9b) moved ontoorigin/mainatfcb39a9, before #511 (c853d2f) landed - the merge script rebasespast it. One conflict,
native/src/sql/sim/mod.rs'sUNRESOLVED_BITS:origin/mainalready carriesDM_SAME_TARGETandPLAT_NEXT_HIGHEST_OVERFLOW(from #509 and #522), and this branch'sown first commit removes
DM_SAME_TARGETin favour of the threadingthis PR adds. Kept
PLAT_NEXT_HIGHEST_OVERFLOW, droppedDM_SAME_TARGET's const and array entry, array size 36 to 35.native load --freshand the full suite, against the throwawaycontainer:
native-sim-candnative-sim-gare the two groups CI runs thisbranch's own named suites under;
sim_missile_same_target_liveandsim_missile_wall_liveare new files neither group's binary filternames yet, so run directly alongside
sim_player_damage_live(thetwo-fireball arm this PR's own threading change affects) and
sim_justattacked_live(the seed guard this PR added catches a realgap in).
Invariants
None. No
PUR-Nrule is touched: every routine here is plain SQL overthe tic's own state row and the tables the load already carries.
Spec impact
Checks
make gates. Ran instead:cargo build --workspace,cargo clippy --workspace --all-targets --features clickhouse-tests,cargo fmt --check,cargo test -p clickdoom-native --lib, and the live tests above, all against a throwaway container - all passing, shown above.make native-smoke- passing, against the same throwaway container.Anything else
Stacked on
native/kills-and-drops(#509, approved, not yet merged),branched from its own head as asked.
native/player-damage(#505) isalso not yet merged; this branch carries #509's own commits ahead of
it, so it needs a rebase once #505 merges, then again once #509 does.
A defect this surfaced and left alone: seeding two independent
in-flight missiles from scratch (not from a probe row, not sharing a
target) on a freshly loaded level, aimed at a third thing on parallel
lines that never touch, only the first missile's own impact ran - the
second's own state never left its spawn frame. Filed as #512 rather
than chased here: it reproduced identically across two different
missile geometries, which points at the two-missiles-in-one-list case
itself, and both the single-missile case (
sim_missile_kill_drop_live.rs)and the two-missiles-on-the-player case this PR fixes (seeded from a
real probe row) work, so it did not block this PR's own evidence.
Written mostly by Claude Sonnet 5.