native: let the zombieman and the shotgun guy shoot - #478
Open
MarcusKainth wants to merge 1 commit into
Open
Conversation
MarcusKainth
marked this pull request as ready for review
September 5, 2026 02:05
MarcusKainth
force-pushed
the
native/hitscan-monsters
branch
from
September 5, 2026 02:05
4694e9d to
b36ddd8
Compare
`A_PosAttack` and `A_SPosAttack` share a shape: `A_FaceTarget`, one `P_AimLineAttack` at the angle the face left, then `P_LineAttack` once for the zombieman and three times for the shotgun guy. Both are one primitive over a list of asks, keyed by the routine the frame carries. Each shot turns the angle by two numbers, the earlier one on the left, and rolls `((P_Random()%5)+1)*3` from a third. The aim draws none of its own, and the puff or blood spot a shot leaves draws four more before the next shot turns its own angle, so the shots are a fold: what a shot draws depends on what the shot before it reached. Step zero of the fold is the aim and every step after it is a shot, so `P_LineAttack`'s walk, its intercepts and their order are in the statement once. The answer carries what the face left, the slope, what each shot reached as a `shoot::reached` answer, what it rolled, and the two bases the caller hands to `mobj::spawn_debris` and `inter::damage_mobj`. `A_FaceTarget` moves out of `attack` into a builder both read, so the angle and the ambush clear are written once. Two paths leave the call stuck rather than guessed: a special line a shot crossed, which is `P_ShootSpecialLine`, and a shot that kills something a later shot of the same call would have reached, which changes what that shot is told. `shoot::REACHED_TYPE` names the answer's type, which a caller carrying a list of them through a fold needs.
MarcusKainth
force-pushed
the
native/hitscan-monsters
branch
from
September 5, 2026 02:14
b36ddd8 to
548d9c7
Compare
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
Part of #376.
A_PosAttackandA_SPosAttackare what the zombieman and theshotgun guy do when their state cycle reaches an attack, and
demo3reachesthe first at gametic 300 and the second at 612.
Both share a shape:
A_FaceTarget, oneP_AimLineAttackat the angle the faceleft, then
P_LineAttackonce for the zombieman and three times for the shotgunguy. Each shot turns the angle by two numbers and rolls
((P_Random()%5)+1)*3from a third.
The shots are a fold, not a map. The aim draws nothing of its own, but the
puff or blood spot a shot leaves draws four numbers before the next shot turns
its own angle, and a shot that reaches a thing draws for the damage on top. How
many numbers a shot draws depends on what the shot before it reached, so the
count has to be threaded. Step zero of that fold is the aim and every step after
it is a shot, which puts
P_LineAttack's walk, its intercepts and their order inthe statement once rather than twice.
The asks are a fold too, so a tic with nothing shooting does not run the
body. That is the shape the cost model asks for when a stage holds a whole
P_LineAttackwalk. The measurement below does not separate it from a map atthis size, and says so.
A_FaceTargetmoves out ofattack. Both routines begin with it, so theangle and the ambush clear are written once and read by both.
What the answer carries is what the tic needs to apply: the angle and flags
the face left, the slope, what each shot reached as a
shoot::reachedanswer,what it rolled, and the two bases the caller hands to
mobj::spawn_debrisandinter::damage_mobj.Two paths leave the call stuck rather than guessed. A special line a shot
crossed is
P_ShootSpecialLine, which is not written. A shot that killssomething a later shot of the same call would have reached changes what that
shot is told, and the shots are worked out against the world as the tic left it.
A_PosAttackfires once and reaches neither.Where this delivers less than #376 asks for. Nothing calls the primitive
yet, so the resident statement does not hold the walk and
native diffstopswhere it did.
A_CPosAttackis not here:E1M7carries no chaingunner.Evidence
ClickHouse 26.8.2.7 (the digest
docker-compose.ymlpins) in a throwawaycontainer of this lane's own on port 18138,
docker/clickhouse/config.dandusers.dmounted as the compose file does. The machine was not quiet, soscripts/machine-lock.shwas taken and given back around each timed run and thearms were interleaved.
What the engine's own trace and ELF say about the draws
refemu's random-call log names the caller of every drawdemo3makes.Gametic 300, a zombieman firing, and gametic 612, a shotgun guy:
Three numbers a shot, the puff's four between one shot and the next, and nothing
at all from
P_AimLineAttack. That is the order the fold puts them in.The offsets name the instructions.
A_PosAttackat80016e98, in the toolchainimage the ROM is built with:
The earlier draw is the left operand, the shift is twenty, and the aim runs
before any of the three and draws nothing.
A_SPosAttackat80016f7cemitsthe same three instructions inside its loop, with the aim's slope held in
s5across all three shots:
The ELF is the pinned one:
The gunshots against the engine
sim_gunshot_liveputs three ofE1M7's own zombiemen and three of its shotgunguys onto the player, leaves one of each with no target, and fires at three
random indices and two draw offsets, with and without
MF_SHADOWon the player.Every number is compared against
native/tests/support/attacks.rs, a readerwritten from
p_enemy.c.The answer carries no shot angle, so where the shots went is checked by asking
the same walk for the angles the reader worked out and comparing what it
reached. What each shot reaches, and what its damage call draws, are what
sim_hitscan_liveandsim_damage_livecheck; the reader takes those as givenand follows the face, the spread, the damage roll and the order the three run in.
The fan's own coverage check fails the run unless it reaches every arm. Its
counts, read by raising one of its thresholds:
A shot that reaches nothing at all is not among them, and the check asserts that:
the level is closed and
MISSILERANGEis longer than any sight line on it, soevery shot ends on a wall or a thing.
The test fails when the behaviour breaks
Five breaks, each one line of the generator.
The spread's two draws swapped:
The damage rolled from the first number rather than the third:
The shotgun guy firing once:
The puff's four numbers not counted:
The face's own two numbers not counted:
What it costs, and what the second walk will cost
A throwaway container of this lane's own on port 18138 at the digest
docker-compose.ymlpins, the machine lock taken and given back around eachrun, three rounds, interleaved arm by arm, every value the minimum of its
rounds.
analysisisQueryAnalysisMicrosecondsfromsystem.query_log, andthe tic is the query's own CPU time with parsing, analysis, planning and
pipeline building taken off, over the tics the window holds.
The last two rows are copies of the statement that wire the primitive into the
tic, one over an empty ask list and one over a single ask every tic. The copies
are for the measurement only; no generator code changed for them.
mainatd824fc9The four rows were measured against
mainatd824fc9, before the rebase ontof2818a7. Nothing in the generator moved in the rebase, so the sizes and thetics stand; what the rebase added is three fields filled in a struct literal,
described below.
The pull request itself adds 504 bytes, which is the two routine ids the ask
switches on, and the tic sits below
mainon the shorter window and 0.26 msabove it on the longer, inside the 0.5 ms a change is allowed.
Wiring it adds about 77,000 bytes and 2.6 s of analysis, and between 0.6 and
1.7 ms a tic. The two wired rows do not separate on this instrument: the rounds
are 1 to 3 ms apart, which is the size of what is being measured, so what the
fold over the asks saves on a tic with nothing shooting is below what these
rounds resolve. The same wiring with the body in a map, measured in an earlier
sweep against that sweep's own
main, read 1.45 ms above it against this one's0.60, which points the same way without settling it.
Every round, as
analysis / cpu tic:Checks
The lane's own suites and the ones that read the same walk or the same face:
290 tests over the unit suite and every live suite, on a throwaway container at
the pinned digest.
The state rows over 300 tics, against
mainatf2818a7from a fresh loadeach, as
hex(cityHash64(...))over the rows ordered by tic, with each side'sbinary built at the commit it names:
Both stop at tic 169 on the same
prndindex, which is the divergencemainalready has.
Invariants
None of the
PUR-Nrules move. The gunshot reads the state row, the leveltables and the pinned constants; nothing reads a clock or a host value, and the
random numbers it draws are the engine's own, at the offsets
p_enemy.cmakesthem.
scripts/check_purity.shruns insidemake lintabove.Spec impact
Checks
make gates. The suites run instead are named abovemake native-smoke, ifnative/ordriver/src/native/changedAnything else
face_targetmoves the face out ofattack, and the same refactor widensattacks::Attackingwithm_z,m_heightandm_health, which the gunshotreads for the shot's own origin. The rebase onto
f2818a7fills those threefields at the one
Attackingliteralmobj.rshas, frommk_m_z,m_heightand
m_health, the same arrays theinter::Hurtingliteral below it reads.attack_foldreads none of the three, so the claw is unchanged andsim_troop_liveandsim_parity_livepass above.The wiring lane W needs.
attacks::hitscan(asks, world, targets, hurting)takes(slot, routine, base)asks, whereroutineisstate_action[1 + state]forthe frame the cycle entered. The answer's
SHOTS,DAMAGE,SPAWN_BASEandHURT_BASErun together, one entry per shot: a shot goes tomobj::spawn_debriswith its own base and, where it reached a thing, tointer::damage_mobjwith the other.DRAWScounts all of it, so whatever thetic does after the call reads that rather than a constant.
shoot::traverseis asked once for the aim and every shot together, so wiringthis adds one walk to the statement and not one per shot.
Written mostly by Claude Opus 5.