Conversation
Re-applies our feeler election + squad push-through + deadlock mitigations on NDA's rewrite, keeping its avoidance_stalled, neutralize_default_avoid, and engine getters. Election-only (density gate dropped); cuts the per-zone CACHE log spam. Parses clean, verified in-game.
gwalls
left a comment
There was a problem hiding this comment.
Carrying over the explanatory notes from #1, rebased onto NDA v3.0. (Dropped the one suggesting we disable feel in the else branch - testing showed that collapses feeling, so the final code keeps feel on the elected feeler regardless of its own detection roll.)
| -- Elect a feeler iff no lower-id stalker (any squad) is within SQUAD_CONTENTION_RADIUS, so | ||
| -- two nearby bolt-throw renders can't contest the skeleton/IK locks. A lone NPC always wins; | ||
| -- in a crowd only the lowest id wins, and winners are always at least the radius apart. | ||
| local function is_feeler_elected(self_id, self_pos, self_squad_id) |
There was a problem hiding this comment.
This was dead code in main (is_cluster_tester). Refactored into the feeler election using our SQUAD_CONTENTION_RADIUS constant.
| -- Mark one NPC as pushing through: drop restrictions/feel and set state. Doesn't touch the | ||
| -- squad rush flag (set_squad_running's job). `single` = in-contact mode (exits when clear of | ||
| -- any damage radius); field mode (stuck) exits only when no anomalies remain nearby. | ||
| local function enter_push_through(npc, st, reason, single) |
There was a problem hiding this comment.
Part of the push_through family: request_push_through -> commit_push_through -> enter_push_through -> exit_push_through.
| if st.last_update and current_time - st.last_update < 1500 then | ||
| return | ||
| end | ||
| st.last_update = current_time |
There was a problem hiding this comment.
Redundant given the 2-second tick - dropped.
| -- * DENY outright if another squad is already pushing nearby (stuck/field commits only), or | ||
| -- * DEFER by FEEL_TO_RUSH_DELAY if the NPC is mid bolt-throw, so its render finishes first. | ||
| -- Otherwise commit now. The actual work is in commit_push_through. | ||
| local function request_push_through(npc, st, reason, single) |
There was a problem hiding this comment.
Considered making this recursive (continuously defer until in the correct state), but I am not seeing freezes in recent tests, so avoiding the added complexity and leaving it as a one-shot.
| end | ||
|
|
||
| if #nearby == 0 then | ||
| reset_avoidance_state(npc, st) |
There was a problem hiding this comment.
Moved to a single helper for resetting state.
| if st.restrictions then clear_npc_restrictions(npc) end | ||
| set_npc_feel(npc, st, false) | ||
| set_squad_rush(npc, true) | ||
| safe_call(npc.set_movement_type, npc, move.run) |
There was a problem hiding this comment.
The maintain block now clears, escalates, or maintains the push-through attempt, and distinguishes rushing through a single anomaly vs an entire field. Handling the whole squad matters or solo members continue in their squad stance (walking).
The key fix is forcing move.run every tick. "Free NPCs maintain run automatically" and "scheme-driven squad NPCs revert to walking anyway" would be nice, but the walking revert is really the bug - an NPC that reverts to walking inside the field takes continuous damage and dies (observed on main). The squad rush flag only moves xr_reach_task NPCs, so I set run directly on every committed member, leader and squadmates both. No animation flicker in testing.
| local grew = has_new_anomaly(detected, known_before) | ||
| clear_npc_restrictions(npc) | ||
| safe_call(npc.add_restrictions, npc, "", tconcat(detected, ",")) | ||
| if grew then safe_call(npc.invalidate_restrictions, npc) end -- 2026.6.1+ API |
There was a problem hiding this comment.
This is what actually removes the flicker, and why a larger MAX_RESTRICTIONS_PER_NPC matters - restriction-list churn (mostly additions) is what creates the flicker seen in NPCs.
| -- accessible pocket, regardless of count. A low cap mainly causes harm: when more anomalies | ||
| -- are in range than it allows, the kept subset shuffles as the NPC moves, re-applying | ||
| -- restrictions every tick and producing a path-rebuild jerk. | ||
| local MAX_RESTRICTIONS_PER_NPC = 30 |
There was a problem hiding this comment.
We do not save much by keeping this small, and a smaller value hurts behavior: smaller caps churn the restriction list more, which I believe causes the animation flicker. Enclosure risk is mitigated by the new rush/clear-restrictions behavior.
| -- this close, so two bolt-throw renders don't contest the CKinematics skeleton/IK locks. Also | ||
| -- the radius for push-through admission (a commit is denied if another squad is pushing within | ||
| -- it). Caps simultaneous bolt-throwers regardless of the engine-side mutex fix. | ||
| local SQUAD_CONTENTION_RADIUS = 10 |
There was a problem hiding this comment.
Once the deadlock is fixed in the MT exes these mitigations can be removed or reduced. (Update: now fixed in xray-monolith #576.)
Distance-gate invalidate_restrictions: only force a reroute for a genuinely new anomaly within half the detection radius. Far rim-entries still get borders set but don't rebuild the path every tick (the ~2s movement stutter). Run the in-contact push-through escape before the should_add_restrictions gate, so static-post / looting / excluded NPCs taking anomaly damage still bolt out instead of standing in the field and dying.
Remember miss outcomes (not just hits), so an undetected anomaly isn't re-rolled every tick until it passes -- it is re-evaluated only after the memory window expires. Rank miss% now means chance to notice on first sighting, not a compounding per-tick roll.
On its first anomaly hit, an elected feeler discovers the zone (forces it into memory) and keeps feeling so the engine back-away can extract it, instead of blind-sprinting deeper into a dense field. Sprints only on a repeat hit or if it isn't a feeler. Gated on st.feel_on so only the per-cell feeler holds (no contact-thrash); unblocked by the merged xray-monolith deadlock fix.
reset_npc_anomaly_state now delegates to reset_avoidance_state instead of duplicating the field-clearing -- also fixes it to clear took_damage/contacted on the out-of-bubble and mod-disable paths. Read immune_zero_miss_chance once per tick instead of once per nearby anomaly.
squad_contention_radius is now an MCM track (0-50, def 4) read at point of use instead of a hardcoded constant. Lower = more feelers; 0 = every NPC feels.
…ction Feel now goes to the squad's scout (lowest-id alive member) and any recent victim (st.contacted), instead of the geographic lowest-id-per-radius election. A first hit makes any NPC discover+hold; it's safe now because a contacted NPC feels via the rule, so the held NPC gets engine back-away as its fallback (the thing the earlier blanket-hold lacked). Removes is_feeler_elected + the per-tick position snapshot.
A commit now pushes only the committing NPC. Squadmates keep their own avoidance (scout/victim feel + restriction routing) and route around instead of being dragged through the field. Removes the propagation loop in commit_push_through; the committer still rushes reliably via the squad rush flag.
Admission control (other_squad_pushing_near + the commit-denied branch) was a pure anti-deadlock band-aid -- moot since xray-monolith #576, and it stranded stuck NPCs. Its removal and the earlier election removal orphan squad_contention_radius, so drop that MCM option (default/group/slider + eng/rus strings) and reword the engine-feel desc to the scout+victim model.
One squadmate's fresh detection routes the rest around a zone without each re-rolling. squad_knows_anomaly consulted only when the NPC has no fresh hit of its own, so it also overrides a personal miss but skips the squad scan when already known. Detection-only; gated on per-NPC memory being on.
It fired every ~2s whenever debug was on, drowning the event logs.
Re-applies the anti-freeze avoidance layer (the work in #1) on top of NDA v3.0, keeping
Priler's rename and Arrival/wall-like-anomaly rewrite, then reworks it from squad-level
coordination to individual behaviour. Clean 4-file diff against the NDA base.
Kept from NDA v3.0
avoidance_stalled(hard-ceiling stall test),neutralize_default_avoid(type 2->3 wall fix),is_enabled_anomaly/inside_anomalyengine getters, the cache staleness refresh, andreset_npc_anomaly_state.How avoidance works now
bolts) is enabled for the squad's scout (lowest-id alive member), so every squad has one
prober and shapeless fields still get scouted, and for any NPC that just took a hit
(
st.contacted) so it gets engine back-away while it extracts. Feel is decoupled from thedetection roll -- with
g_ai_die_in_anomaly = 0the engine ignores anomalies unless feel ison, and gating feel on the roll collapses to almost no feeling. Detection gates restrictions,
not feel. Replaces both NDA's density gate (orphaned
engine_feel_max_densityMCM optionremoved) and the geographic per-radius feeler election from the first pass.
undetected anomaly isn't re-rolled every tick until it passes. Rank miss% now means "chance to
notice on first sighting" rather than a compounding per-tick roll.
memory) and keeps feeling, so engine back-away can extract it instead of it blind-sprinting
deeper into a dense field. It sprints only on a repeat hit, or immediately if it isn't feeling.
off, per-tick direct
set_movement_type(run)plus the squad rush flag so it actually sprints,single->field escalation if it stays stuck. Squadmates keep their own avoidance and route
around instead of being dragged through the field.
invalidate_restrictionsis distance-gated to a genuinely newanomaly within half the detection radius; far rim-entries get their borders set without a path
rebuild every tick (that was the ~2s movement stutter).
MAX_RESTRICTIONS_PER_NPCis 30.taking anomaly damage bolt out instead of standing in the field and dying.
the rest without each re-rolling. Consulted only when the NPC has no fresh hit of its own, so
it overrides a personal miss but skips the squad scan when the zone is already known.
Detection-only; requires per-NPC memory to be on.
Dropped since the first pass
Feeler SpacingMCM option -- superseded by scout + victim feel.themrdemonized/xray-monolith#576
merged, and it stranded stuck NPCs.
[CACHE]log line (hundreds/tick at verbose; theRebuiltsummaryalready aggregates), and the per-tick
[HEARTBEAT]line is now behind verbose debug.FEEL_TO_RUSH_DELAY(1.5s between feel-off and the rush flag) stays: it lets an in-flightbolt-throw animation finish so the
CMissileleaves the hand HudItem slot before the movementtransition, which also avoids the run-while-throwing glitch.
Notes
CKinematicsskeleton-mutex deadlock is fixed engine-side in xray-monolith#576; theremaining mitigations here are a fallback.
add_restrictionsonly blocks a zone whose shape catchesan AI-navmesh vertex. A functional border probe (2026-07-03, Jupiter, on exes with the rt=3
fixes) confirmed this is per-zone, not per damage type -- a gravitational zone came back
with an empty border while a thermal one blocked correctly. Empty-border zones can't be routed
around by restrictions at all, so they fall back to feel + sprint-through and avoidance there
stays unreliable. Follow-up: probe-classify each zone at cache time and skip restrictions for
empty-border ones.
Testing
Parses clean under LuaJIT. In-game: clean load (no script errors), steady scout feeling,
push-through + rush, restriction stabilization, NPCs still die in anomalies.