matiaszanolli

Gameplay Audit (P2/P3 slice, M41–M42)

"Deep audit of the gameplay layer in byroredux/src — inventory/equipment, interaction/locks/loot persistence, consumables, combat + death, NPC spawn → AI packages → locomotion, player feedback, gameplay-state save coverage and stage order"

matiaszanolli 28 3 Actualitzat fa 1 setm.
GitHub

Instal·la

npx skillscat add matiaszanolli/byroredux/claude-commands-audit-gameplay

Instal·la des del registre de SkillsCat.

SKILL.md

Gameplay Audit (P2/P3 slice, M41–M42)

Read _audit-common.md and _audit-severity.md for shared protocol.

Audits what the player and NPCs do once content loads. Elsewhere: CHARAL formulas → /audit-character; save schema/atomicity → /audit-save (this skill owns what should be restored); lock order / declared access → /audit-ecs + /audit-concurrency; Scaleform/MenuXml rendering → /audit-ui; KCC/ragdoll → /audit-physics; ALCH/MGEF/LVLI/PACK decoders → /audit-esm; Papyrus/ObScript → /audit-scripting.

Architecture: one Task agent per dimension (max 3). Delta-first: skip a dimension whose Paths are unchanged since the last docs/audits/AUDIT_GAMEPLAY_*.md and whose Guard is green (unless --depth deep).

Ground truth (docs lag; re-verify)

docs/engine/npc-spawn-ai-packages.md, docs/engine/packal.md, docs/engine/dialogue-trees.md, docs/engine/playable-vertical-slice.md, docs/feature-matrix.md. Smoke gates (Vulkan + data): docs/smoke-tests/{p0-door-interaction,p2-melee-core,p3-player-body,p4-quest-route,p5-save-restart,p6-loading-model}.sh (P0–P5 closed; the slice doc is the gate-of-record), plus docs/smoke-tests/{dt1-dialogue-layers,dt2-skyrim-forcegreet,m42-eat-sleep}.sh for the dialogue layers and Eat/Sleep. Never launch a second engine beside the user's.

Known-open (verified 2026-10-12; cite, don't re-file): #4232 effective_actor_level returns 0 verbatim, emptying leveled lists. #5367 (greeting G, line lifetime L, force-greet F, FO3/FNV voice V, Skyrim force-greet in 287214103) is closed, and so are its follow-ups: #5429 (FO3 is pinned by the ignored real-data floor dialogue_greeting_and_forcegreet_fo3_floor in crates/plugin/tests/parse_real_esm.rs; dt1-dialogue-layers.sh fo3 is a declared SKIP, c6e48cbc6), #5433 (the FO3 profile ships its sound + voice archives, 4ef386a1e), #5521 (the voice-name truncation rule, 4d6294bb1) and #5432 (a voice line is cut on topic change, close and death, 40b77828b). What remains is filed separately: Skyrim+ .fuz voice is unbuilt (its container description is disputed in #5464) and Oblivion voice is unbuilt (mp3 race/sex layout recorded in docs/engine/dialogue-trees.md; no VTCK/VTYP, #5526). #4415 (magic runtime) is closed — its last scope, the racial spell pair (RaceSpells, AddRaceSpells/RemoveRaceSpells), landed in 9813af435; RaceSpells is spawn-derived (NPC placement and, since #5413 212a71dba, the player's attach_to_player) and deliberately unsaved. Reviving a player whose body death already ragdolled has no ragdoll-deactivate path (7aa1d7741). No pickpocket / theft-alarm system exists, so #5017's unconscious clauses for them have no hook. Seven of ~17 PACK procedures (Find/Accompany/UseItemAt/Ambush/FleeNotCombat/CastMagic/UseWeapon) have no runtime — FO4+ walk clips are absent, cross-tile pathing is blocked (docs/engine/navmesh-pathfinding.md §9): a finding is a silent misroute of such a case, not the absence.

Extra field: Trigger (game, cell, save state).

Phase 1: Setup

  1. mkdir -p /tmp/audit/gameplay; save gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels to /tmp/audit/gameplay/issues.json and dedup every finding against it (_audit-common.md); read the newest docs/audits/AUDIT_GAMEPLAY_*.md.
  2. Baseline cargo test -p byroredux -- combat:: inventory:: interaction:: npc_spawn:: reference_state ambient_locomotion walk_anim eat_sleep forcegreet dialogue_voice save_io::.

Phase 2: Dimensions

Dimension 1: Inventory & Equipment Model

Paths: byroredux/src/inventory.rs, crates/core/src/ecs/components/inventory.rs, crates/core/src/ecs/resources/mod.rs (ItemInstancePool), crates/plugin/src/equip.rs, byroredux/src/player_body.rs, byroredux/src/npc_spawn/loot_appearance.rs (GearImportLoader)
First step: git log --since=<last> --format='%h %cs %s' -- byroredux/src/inventory.rs crates/core/src/ecs/components/inventory.rs
Guard: inventory::tests::{armor_toggles_never_touch_the_equipped_weapon, load_reconciler_*}, core no_armor_mask_can_reach_the_weapon_slot.

  • Canonical state is Inventory + EquipmentSlots + EquippedWeapon (core). InventoryCatalog and the two player templates are read-only, rebuilt by install_catalog per content load; a mutable inventory copy in the binary is a boundary violation.
  • Rows are zeroed in place, never removed (equipment and EquippedWeapon.inventory_index point into items): check consume, transfer_loot, pickup_loot, reference_state::restore. Slot mutations re-derive EquippedWeapon (reconcile_equipped_weapon); armor toggles must not.
  • ItemInstance has no fields (_reserved); production allocates only in restore. Reject claims that per-instance condition/mods/charge exist; Some(instance) paths must still be atomic.
  • Player body + mid-life gear (P3): attach_player_body builds a strictly visual body via NpcSpawnJob (player_body flag: no identity/inventory/AI/collider stamps; the capsule stays the only physics body). An equip with no NpcEquipmentPart root queues PendingGearImport → GearImportLoader (retained NpcSkeletonBones/ActorBodyClass); inv.equip must go through apply_action. The body comes from the NPC_ 0x7 record, read through its TPLT terminals like the player templates (#5047, c20dcdb04). Post-load, reconcile_worn_gear (loot_appearance.rs, #5034 20717d5b7) diffs live non-intrinsic roots against the restored EquipmentSlots (reveal / hide / queue PendingGearImport) from reference_state::restore (NPCs) and the post-apply_deltas player block — synthesized in memory, never an EquipmentEventBatch. Mid-life gear is released: a leaves-inventory transfer on a cellless wearer queues PendingGearRelease → GearImportLoader → cell_loader::unload::release_entities (#5028 f78e018ac; NPC gear stays cell-owned). NpcEquipmentPart has no inventory_index (#5049 dropped it; hide/reveal match on form_id). reconcile_worn_gear builds equipped_forms from biped slots only, in slot order (the weapon slot never has a mesh, so including it starved the reconcile and made missing nondeterministic, #5266 6e0594e68); a zero-mask item never reaches occupants, so a mis-decoded biped mask hides that gear on every restore (Skyrim's pre-BOD2 BODT decodes since #5358, 479414ffe); queue_midlife_imports resolves before the per-wearer pick so an unservable form cannot strand servable ones. A batch is folded as a transition LOG (an unequip cancels a queued equip of the same form), GearImportLoader re-checks the live slots (equipped_forms) before every path and drops an import whose item is no longer worn, and a file-level miss or failed import chains reconcile_worn_gear_excluding with the failed form held out so the forms behind it never stall (#5031 ec41e33e8; an_unequip_in_the_batch_cancels_the_queued_equip, a_failed_import_does_not_stall_the_forms_behind_it). The gear-release subtree walk uses core's guarded collect_subtree (#5513 9da80470d). The gear-release "still held" filter counts only count > 0 rows (zeroed rows stay, #5268 fec2d14b2); the added == false release path is forward-latent (transfer_loot source side is the only producer). The pre-baked FaceGen miss assembles a head from the NPC's PNAM HDPT parts (PrebakedPhase::HeadParts, PrebakedHeadPart; the Skyrim player has no facegeom so this is its normal path; #5095 8c925ec54); npc_spawn::is_child_race is the one shared FO3/FNV Child translation for spawn and player body (#5079).

Dimension 2: Interaction, Locks, Containers & Loot Persistence

Paths: byroredux/src/interaction.rs (action half), byroredux/src/systems/{npc_dialogue,forcegreet,dialogue_voice}.rs, byroredux/src/cell_loader/{reference_state,unload}.rs, cell_loader/references/{attach,synth_child}.rs, byroredux/src/npc_spawn/loot_appearance.rs, crates/core/src/ecs/components/lock.rs, crates/plugin/src/equip.rs
First step: cargo test -p byroredux -- reference_state scripted_lock_gate synth_child interaction:: forcegreet dialogue_voice
Guard: reference_state::tests (12), synth_child_tests::actor_placement_stamp_restores_only_the_looted_instance_of_a_shared_base, scripted_lock_gate_tests, interaction::tests::key_gate_rejects_missing_zero_count_wrong_and_keyless_locks, loot_appearance::tests.

  • Pipeline: interaction_system (ahead of every OnActivate consumer) → ActivateEvent (read, not drained) → container_loot_system → transfer_loot (validates source/lock/both inventories before mutating; whole stacks; source equipment cleared, one unequip event each) or pickup_loot.
  • Locks: key unlock happens on activation, not prompt, and is recorded in ReferenceLockState (saved). carried_unlock_key reads PapyrusPlayerEntity; loot/combat read PlayerEntity (both set in scene.rs; the fly-cam placeholder has no Inventory) — can they diverge? lock_level has no consumer.
  • Persistence: capture runs before unload frees instances; restore at attach consumes the row; without_parked_state shields a save reload; parked rows own their payloads; identity is the REFR FormIdPair, not the base. Every new placement fact (lock override, Dead, picked_up, inventory, equipment, AVs) needs a row field or ledger, else eviction loses it. No serde defaults on ReferenceState (#4465): schema change ⇒ FORMAT_MAJOR bump (/audit-save).
  • Leveled loot: containers and NPC CNTO carry lists (#4696) both use expand_leveled_loot (LVLO counts multiplied, Use-All 0x04); only the worn outfit (DOFT/OTFT) keeps the flat expand_leveled_form_id. A new LVLI call site that takes the flat walk for a carried list is the regression.
  • NpcLootAppearance hides originals only once the replacement is fully staged; never strips a living actor.
  • NPC dialogue (P4, closed): Late npc_dialogue_selection_system reads ActivateEvent (before event_cleanup_system), walks running-quest DialRecord::quest_refs bound to the NPC, stamps NpcDialogueTopic into DialogueRegistry; DialogueSurfaceState keys the surface on its npc and apply_selection strips every other NPC's topic (#5038). Topic reachability follows the CK branch model (#5037, 7ab87c0fb; module doc is the spec): only DialogueCategory::Topic with an INFO passing for this NPC; a qualifying Blocking DLBR entry opens and its TCLT links replace the list; else Top-Level starting topics; FO3/FNV topics need the DIAL DATA Top-level bit 0x02 (#5224, 305363122: choice-only topics stay link-only). When no owned quest topic qualifies, open_conversation falls back to the master's generic greeting topic (GREETING on FO3/FNV, DialogueGenericHello on Skyrim; #5367 G, 14cff35ae), resolved once per load order into EsmIndex::generic_greeting_form and borrowed, not cloned (#5426, 411d0eeaf). FO3/FNV INFOs gate on their OWN quest's running state (QSTI → InfoRecord.quest), ordered by DialogueQuestPriorities (installed once per load; absent = file order) with conditions evaluated against that quest (#5271 dbc07e8f0). select_info collects every passing candidate: the first passing INFO wins and a Random first INFO rolls within its positional Random stack (ended by the next passing non-Random INFO or a Random End, #5397 0436bea5c) through the seeded DialogueRandomState, spoken Say-Once INFOs are disqualified via the saved DialogueSpokenInfoForms ledger of (speaker REFR FormID, INFO) pairs — once per actor, the CK rule; Skyrim INFO flags decode from ENAM or the legacy 8-byte DATA; FORMAT_MAJOR 38 (#5300 f18224b02) — and a spoken Goodbye closes the conversation when its presentation estimate (or, with voice, the real audio duration) elapses (#5367 L). Activation selection, topic click and forcegreet_open (#5392, 16544d4c9) gate on player_can_act, and npc_refuses_dialogue (dead, AiCombatState, unconscious #5017) is shared by those three and the interaction Talk arm (#5043) — a new open path without both is the regression. Spoken lines run their INFO VMAD TIF_ fragments (OnBegin at selection, OnEnd on change/close, and also when the same INFO is re-selected — every existing stamp ends first, #5300; #5152 via DialogueInfoFragments); the spoken path returns direct advances WITHOUT polling the fragment journal in Late (polling claimed the line's own SetStage, #5297 68a06509a) and the system's Access row declares the fragment queues / SceneActorBindings as writes (#5307, pinned by npc_dialogue_selection_declares_everything_the_spoken_fragment_path_acquires). Force-greet (PACK procedure 15, #5367 F): forcegreet_system (Update exclusive) walks a ForceGreetDirective carrier to the player and opens the package's PKDD topic (or the generic greeting) through the same selection via forcegreet_open. Installers: ambient AmbientBehavior::Dialogue, only for a fully-modeled (no fail-open pass), player-targeted (PTDT 0x14 or none), Conversation-type (PKDD type 0) package — fail-open had armed 60 FNV bases (#5376, 4e58f241d, guard dialogue_ambient_install_declines_unmodeled_offtarget_sayto); selection itself skips a Dialogue package whose pass was manufactured by fail-open and continues to the next eligible package, so a declined one no longer starves the Sandbox/Travel/Eat/Sleep packages below it (#5499 f7ca4a60f) — and the unconditional dialogue.forcegreet console door, which also accepts the Skyrim ForceGreet procedure-tree leaf via PackRecord::force_greet (287214103; the ambient arm does not wire the tree dialect). A failed open consumes the directive; the directive is unsaved by design. The system holds every directive while player_can_act fails and holds one in radius while the player talks to another NPC (consumed if already talking to the greeter; #5392, guards dead_player_holds_the_forcegreet_directive, open_conversation_holds_or_consumes_the_forcegreet_directive). Its walk rides navmesh_path::cached_walk_step (cached NAVM path, KCC blocked reported: a greeter held against an obstacle for LOCOMOTION_STUCK_REPICK_SECS opens in place, #5452 6316719ed), writes Transform and takes PhysicsWorld only for the step (#5373/#5371, f0c683ef1; forcegreet_walk_survives_transform_propagation). PackRecord::force_greet takes the caller-resolved PKCU template: a Skyrim concrete package names its behaviour through the template and carries only data-input values, so the ForceGreet leaf is sought on the package's own tree then the template's, the topic read from the instance's PDTO first (334 of the 339 vanilla force-greet packs are template instances, #5511 72e863d80). Voice (#5367 V, dialogue_voice.rs, called from apply_selection): sound\voice\<owning plugin>\<VTCK→VTYP EDID>\<quest EDID>_<topic EDID>_<plugin-local INFO id>_<response_number>.ogg (the FO3/FNV convention; Oblivion's mp3 race/sex layout is unbuilt, #5526), where both EDIDs are cut (quest to 10, the pair to 25) ONLY when quest + topic exceed 25 — a length test with exactly one candidate, not a radio-quest exception (#5395, #5521 4d6294bb1; voice_path_candidates_truncate_only_over_the_length_limit); the owner resolves through GlobalFormIdResolver, not the FormID's top byte (#5393; 5b68792b7). Decodes go to the byte-budgeted VoiceSoundCache behind an AudioWorld::is_active gate (#5382), segments are anchored to the speaker with play_oneshot_following and skipped when it has no GlobalTransform (#5410), and stop_line_voice (a 150 ms-fade wrapper over AudioWorld::stop_sounds_for) cuts the line when apply_selection replaces it (every stamped speaker plus the selecting NPC's own prior line, BEFORE the new segments queue), when end_open_conversation closes it, and when reconcile_dead_actor buries its speaker (#5432 40b77828b; replacing_a_line_stops_the_old_voice_before_queueing_the_new_one, closing_the_conversation_stops_the_voice, dead_actor_reconciler_stops_the_actors_voice_line) — a new line-ending path without it is the regression. Missing voice falls back to the estimate silently. Saved: only DialogueSpokenInfoForms (the rest, incl. DialogueRegistry, is session state). INFO/CTDA semantics → /audit-scripting.

Dimension 3: Consumables & Timed Effects

Paths: byroredux/src/inventory.rs (consume_item), byroredux/src/systems/restoration.rs, crates/core/src/ecs/components/restoration.rs, crates/core/src/ecs/resources/hardcore.rs, crates/plugin/src/consumables.rs
First step: cargo test -p byroredux -- consum restoration timed_; cargo test -p byroredux-plugin consumables
Guard: unavailable_consumption_never_changes_health_or_count, consuming_invalid_instance_is_atomic, save_io/consumable_tests.rs. The Hardcore/perk test real_stimpaks_restore_scaled_health_and_limbs_but_hardcore_only_health is #[ignore]d (FO3+NV data): it guards nothing unless run --ignored.

  • Chain: ALCH EFID/EFIT/CTDA → ConsumableEffect (Skyrim, FO3/NV only) → restoration_plan → InventoryCatalog.restorations → consume_item → instant restore or TimedRestorations (constant rate, simulation seconds, no overheal banking) → restoration_system (clears on Dead/health ≤ 0).
  • restoration_plan is all-or-nothing: any unsupported effect, condition, area, delivery, script or AV ⇒ None, never a partial potion. Conditions fail-CLOSED (GetHasPerk 448/449; IsHardcore 586 on the NV profile only); package CTDA is fail-OPEN (Dim 5). The asymmetry is deliberate; a flip is a finding.
  • Hardcore is an effect CTDA, not a branch in consume_item; HardcoreMode is a saved flag only. Conditions and Medicine scaling are evaluated at use and the resolved rate stored, so later skill changes do not rewrite a dose.
  • AV map in restoration() (Skyrim 24/25/26, FO3/NV 16/12) vs docs/engine/charal-*-ruleset.md; Oblivion/FO4+ Aid items are "effects unavailable" by design. Unavailable only reaches log::warn! (main.rs): #4458 (player lacked AVs) looked like a dead button.

Dimension 4: Combat & Death Pipeline

Paths: byroredux/src/combat.rs, byroredux/src/systems/{combat_ai,faction_hostility,combat_anim}.rs, crates/scripting/src/combat.rs, death producers in byroredux/src/systems/{water,character}.rs
First step: grep -rn 'Dead)' byroredux/src --include='*.rs' | grep -v test — every production Dead insert must reach reconcile_dead_actor (in an exclusive, or queue_dead_actor_reconciliation → Late reconcile_pending_dead_actors_system). Today: combat_damage_system, NPC drowning (water.rs) and player drowning (character.rs), reference_state::restore + apply_starts_dead (#4814 ACHR 0x200, #5013 Oblivion base 0x80000, #5223 FO3/FNV dismember-trigger / kill-on-load scripts via ScriptKilledCorpseForms, computed ONCE per load order over every cell — the persistent-CELL target and its exterior-grid trigger never share a load_references_budgeted call, #5248 d4e8c31be; matched on the exact 11 vanilla GenericBiped*DismembermentSCRIPT EDIDs whose WINNING SCPT writer is a shipped master — LoadOrder::script_winner, the per-SCPT last-writer table, so a true override that keeps the master's FormID is declined (#5304 2d47bf7f1, #5428 335a0179c; kill_on_load_recognition_declines_a_true_override_of_a_vanilla_script); the whole-load-order scan returns empty on every game but FO3/FNV (#5450 eaac75911); FO3/FNV XLKR is 4 bytes), the SDK AV batch (extensions/commands.rs, #4702). The four live producers (not restore) also raise the Story Manager KILL through story_events::raise_kill_story_event with the session location (#5434 c85d50356); a fifth producer that skips it is the regression.
Guard: combat::tests::{consumer_applies_the_producers_damage_rather_than_recomputing_it, dead_state_reconciliation_removes_respawned_ai}, combat_ai::tests::{physics_world_is_taken_only_between_the_storage_passes, a_same_frame_hit_on_the_target_survives_and_the_strike_lands_next_frame}.

  • One damage path: combat_input_system and npc_combat_ai_system emit HitEvent; only combat_damage_system writes Health/Dead. HitEvent cleanup is Late event_cleanup_system — not a leak.
  • reconcile_dead_actor teardown = clear_ambient_behavior set + AmbientPackageRuntime + EvaluatePackageRequest + AiCombatState + AnimationPlayer/AnimationStack (actor and skeleton root) + ragdoll; it must grow with each new per-actor runtime component (Dim 5) — check AmbientEngagement, NpcDialogueTopic, PendingGearImport; it also cuts the dying speaker's voice line (stop_line_voice, #5432).
  • Combat start: Effect::StartCombat or faction_hostility_system (#4414; every EVALUATION_PERIOD_SECS, range from DetectionConfig GMSTs + line of sight, no DetectionConfig ⇒ no ambient hostility). Ambient combat carries AmbientEngagement and drops after DISENGAGE_GRACE_SECS out of contact (#4816); scripted combat never auto-drops — Effect::StartCombat removes the marker so a quest taking over an ambient fight owns it (#5046, fef67f3c3). Unconscious actors (#5017, ActorControlState.unconscious, mutually exclusive with restrained): combat AI drops an unconscious attacker's combat, hostility never arms an unconscious perceiver but may target it and arms it once awake, ambient AI suspends its procedure. suspend_ambient_behavior_for_combat must pause the package while fighting and let it resume (#4703).
  • attack_damage: weapon + CHARAL melee bonus; CreatureAttack only when unarmed; else UNARMED_DAMAGE (formulas → /audit-character). MeleeState is per combatant; PhysicsWorld is taken only between storage passes (#4325). The swing resolves through cast_ray_corridor (MELEE_SWING_CORRIDOR_RADIUS_BU 12 BU ball, #5160) excluding only the aggressor's capsule body — its own bones can be hit, which resolve_actor_root turns into a miss; player rays originate at the eye (player_camera_boom, #5124). Combat take/sound feedback registers outside the locomotion kill switch (#4709).

Dimension 5: NPC Spawn → AI Package Selection → Locomotion

Paths: byroredux/src/npc_spawn.rs, npc_spawn/ai_package.rs, npc_spawn/resumable/, byroredux/src/systems/{sandbox,wander,travel,follow,escort,guard,patrol,eat_sleep,walk_anim,locomotion}.rs, crates/core/src/ecs/components/{sandbox,furniture,wander,travel,follow,escort,guard,patrol,eat_sleep}.rs, boot/schedule/post_update.rs, crates/plugin/src/esm/records/misc/pack.rs (active_package)
First step: cargo test -p byroredux -- ambient_locomotion npc_spawn::ai_package walk_anim locomotion:: eat_sleep
Guard: boot/schedule/mod.rs::ambient_locomotion_default_on_tests::{locomotion_registers_behind_the_single_kill_switch, walk_animation_registers_after_all_seven_movers} (live source-shape) pin registration and order, not procedure semantics. ai_package::tests::{same_winner_preserves_runtime_state, unknown_condition_function_remains_fail_open, eat_sleep_dialogue_procedures_install_and_hand_over}, walk_anim::tests, eat_sleep::tests.

  • Selection: apply_ai_package_behavior at spawn (TPLT Use AI Packages chain, #4093), then ambient_ai_package_system (Update) at most once per in-game minute or on EvaluatePackageRequest, swapping only when the winning PACK FormID changes; QUST alias packages override the base stack; an empty registry at spawn must not erase the winner. active_package = first by priority (skipping a fail-open Dialogue package, #5499) whose PSDT covers GameTimeRes.hour with passing CTDA (fail-OPEN in package_conditions_pass: flag a flip, not the policy).
  • One behavior per actor by construction (AmbientBehavior, ten arms incl. Eat/Sleep/Dialogue), but nothing ties its insert_at_spawn/insert_at_runtime arms, from_package's procedure chain, clear_ambient_behavior's removals (incl. EatSleepState, ForceGreetDirective, WalkStuckTimer/NavPath) and crates/debug-server/src/registration.rs: diff all four per procedure. Skyrim+ tree packages resolve only Sandbox + Patrol leaves (.find(), no per-leaf CTDA; docs/engine/packal.md).
  • Follow / Escort-collect re-read the target per tick; Travel / Escort-lead / Guard freeze once; Patrol is Wander's generic oscillating_walk_system_inner::<PatrolBehavior> and the six walk-to-point movers share one Pass-2 write-back (navmesh_path::WalkDecision + write_back_walk; a mover writing Transform/NavPath itself is the regression, pinned by walk_movers_share_one_pass_two_write_back, #5580 badc45309); prune_seat_reservations keeps furniture and claimant liveness.
  • Eat/Sleep (procedures 3/4, eat_sleep_system, PostUpdate exclusive): walk once to the EatSleepLocation anchor — NearReference ref, NearLinkedReference (type 6: the actor's own XLKR target through PackageTargetRegistry::linked_reference), the spawn-stamped EditorPlacement (never moved, unsaved), InCell (idles unless that cell is resident; its seat search then spans the whole cell, since every In-Cell package authors radius 0), else stay put; an anchor not yet resolvable is NEVER cached as the actor's position — it idles on an ANCHOR_RETRY_SECS clock and retries (#5500 bcaf63aa8, #5527 959b76f55); no random walk (#5391, 42aab4c09) — then seat through the sandbox path (collect_marker_seats / pick_nearest_seat / apply_seat_assignments). The walk rides navmesh_path::cached_walk_step and consumes the KCC blocked flag (held for LOCOMOTION_STUCK_REPICK_SECS ⇒ attempt the seat pick when inside the search radius, #5452 6316719ed); failed seat attempts retry on a coarse SEAT_RETRY_SECS clock over one shared marker gather per frame (#5449 35a009431). Creatures (CreatureActor) are meant never to seat — sandbox_seat_system_inner skips them before any scan and seat_at_marker returns after the walk — keeping Sandbox's wander/idle and Eat/Sleep's authored location (#5495 9bb8bc475). Verified 2026-10-12: nothing in production inserts CreatureActor (the commit and the registry_completeness_tests row say prepare_creature_state stamps it, but that function never does; only the sandbox test inserts it), so the gate is dormant and 59 FNV creature placements would still seat — re-grep before trusting the fix. A stream-boundary restore of a Seated actor reruns the seating write path (sit pose, heading snap, marker re-reservation) or drops Seated for the package to re-seat (#5501 b1fed839c). Sleep prefers FurnitureMarkerKind::Sleep, reachable only on Skyrim+ (animation_type == 2): on FO3/FNV the sit fallback IS the sleep path, chairs included (#5390, 9bdfdd1c6, guard legacy_sleep_actor_seats_at_the_sit_fallback). The walk writes Transform and holds PhysicsWorld only around the step (#5373/#5371, f0c683ef1); its batched gather takes Transform first like every other mover (#5535 1cb12fb3d).
  • Locomotion: speed is WalkSpeed (clip stride, clamp [30,250]) else LOCOMOTION_WALK_SPEED, combat chase included; advance_stuck_repick (2.5 s) serves only Wander/Patrol — Travel/Follow/Escort/Guard drop blocked (Eat/Sleep and force-greet consume it through cached_walk_step, #5452). BYRO_NO_AI_LOCOMOTION=1 is the only switch; the old per-procedure vars must stay unread. Its guard locomotion_kill_switch_gates_exactly_the_motion_systems pins the nine in-block movers (incl. the bare-path eat_sleep::eat_sleep_system) and that forcegreet's Update walk takes the switch via make_forcegreet_system(ai_locomotion_enabled()) (#5430); combat chase and cinematic routes are deliberately outside it.
  • walk_anim: take (0.1 s) / restore (0.4 s) / yield / abandon (seated, dead, cinematic); module doc now states WalkSpeed and the seated chase (#4710, f002763b4). Both spawn paths in npc_spawn/resumable/ (prebaked.rs, runtime.rs) stamp WalkAnimation + WalkSpeed, and share the Armor and Skeleton phase units from resumable/mod.rs (SpawnArmor, spawn_armor_unit, spawn_skeleton_unit; #5312 2f1a76e08 — a fork of either arm's Armor/Skeleton phase is the regression). FO4 NPC_ template flags resolve through the per-flag TPTA actors (NpcRecord.template_actors, 0 = use the TPLT default, #5498 99c22a8db).

Dimension 6: Player Feedback & Flow

Paths: byroredux/src/{notifications,loading_screen,hud,objectives}.rs, byroredux/src/systems/loading_model.rs, byroredux/src/commands/{gameplay,hud}.rs, byroredux/src/scene/character_spawn.rs (plan_character_spawn), byroredux/src/scene/spawn_tests.rs
First step: cargo test -p byroredux -- notifications loading_screen spawn_tests
Guard: notifications::tests::feedback_is_bounded_ordered_and_consumed_once, loading_screen::tests::selector_never_widens_restricted_or_malformed_screens, spawn_tests::door_threshold_floor_does_not_certify_an_overlapping_capsule.

  • PlayerNotifications: 8-deep, push no-ops without the resource, drained once per frame in app_frame.rs into the egui overlay only, with objectives::drain_transition_notifications (#5153 journal announcements on false→true objective flips; the quest label degrades to the FormID form when QuestDefinitionRegistry is absent instead of panicking the per-frame drain, #5286 61b4274da, transition_drain_survives_an_absent_definition_registry) spliced ahead — check that a --hud run without debug-ui still drains and every producer (interaction, inventory, save_io, objectives) reaches it.
  • hud::fraction reads PlayerEntity only (#4675) and, like the debug vitals and the SDK projection, composes the ceiling through the one ActorValue::undamaged() (current = undamaged − damage, set_override layer included, #5508 627149b62): a new vitals consumer must do both. objectives::ObjectiveHudCache rebuilds only when a QuestRevision of QuestStageState/QuestObjectiveState/QuestDefinitionRegistry changes (guard hud_cache_reuses_the_list_until_quest_state_changes): a quest mutation that skips its revision bump leaves a stale objective line. Rendering → /audit-ui.
  • Loading screen: Phase × Owner never strands a cover. Two backends share one selector: legacy flat ICON (Oblivion/FO3/FNV) and the Creation-era LSCR model cover (Skyrim inline pose, FO4 TNAM→TRNS; e60911864, systems/loading_model.rs; the turntable runs from a cover-owned wall clock, not scheduler dt, #5306 c8c0fe868) whose stage teardown goes through release_entities (#5193; its subtree walk is core's guarded collect_subtree, #5513) and releases an image cover it replaces (#5206).
  • Spawn: explicit camera column > nearest door > fallback, with capsule_overlaps_solid clearance; interiors spawn at the first door's own placement (interior_spawn_point_fix). SetInChargen save refusal → /audit-save; settings_io.rs is a shim over crates/settings-io.

Dimension 7: Gameplay-State Coverage, Stage Order & Determinism

Paths: byroredux/src/save_io.rs (registrations), byroredux/src/save_io/{registry_completeness,round_trip}_tests.rs, byroredux/src/boot/schedule/{update,post_update,late}.rs
First step: grep -nE 'Combat|Melee|Walk|Ambient|Seat|Loot|Picked|Notif|Interaction|Faction' byroredux/src/save_io/registry_completeness_tests.rs
Guard: registry_completeness_tests::every_component_or_resource_impl_is_saved_or_explicitly_allowlisted and round_trip_tests::npc_spawn_stamped_components_are_saved_or_intentionally_rederived prove a name is classified, not that its stated rebuild path exists. scheduler_access_tests::p2_gameplay_exclusives_declare_non_empty_access covers three of the gameplay exclusives.

  • Prove the claimed re-derivation for each gameplay allowlist row (AmbientPackageRuntime first tick, WalkAnimation from the spawn lookup, PickedUp re-stamped by restore, EatSleepState re-resolved from the same PLDT anchor and EditorPlacement re-stamped at spawn, #5391). Genuinely unsaved: AiCombatState (session EntityId target) — ambient combat is re-derived by faction_hostility_system after load, a scripted StartCombat against a not-otherwise-hostile target is lost. Saved: DialogueSpokenInfoForms, TimedRestorations, HardcoreMode, PersistentReferenceStates, ReferenceLockState, FactionRelations; Dead and ActorControlState are replacing columns so a saved absence clears them on the process-lifetime player (#5027/#5052, 7aa1d7741).
  • The load overlay is additive-only except replacing columns: a runtime removal that follows from a saved fact must be re-derived after load (models: reconcile_dead_actor_runtime_state, reconcile_player_equipped_weapon, reconcile_worn_gear, the player FactionRanks reset to the record (#5058), the pre-teardown cinematic purge (#5056)). Snapshot rows whose FormIdPair the reload cannot resolve (hysteresis band) are parked into PersistentReferenceStates (#5054) — a new per-reference fact must reach that park path as well as capture.
  • Update order is mostly comment-enforced: restoration_system → interaction_system → fragment_activation_flush_system → container_loot_system → combat_input_system → faction_hostility_system → npc_combat_ai_system → combat_damage_system (only flush-before-consumers is pinned, by activation_flush_is_scheduled_before_every_activate_event_consumer); ambient_ai_package_system → forcegreet_system → scene_package_system (Update exclusives; ambient → force-greet → Late selection is pinned by scheduler_access_tests::story_dispatch_and_forcegreet_update_ordering_is_pinned, #5415 a9f94455d); PostUpdate movers incl. eat_sleep_system precede npc_walk_animation_system; Late water_damage_system → reconcile_pending_dead_actors_system → equipment_appearance_system → npc_dialogue_selection_system → event_cleanup_system. Verify each consumer sees its producer the same frame; inventory::apply_action runs between frames with &mut World.
  • Determinism: grep -nE 'rand::|Instant::now|SystemTime' over combat.rs, inventory.rs, interaction.rs, ai_package.rs, faction_hostility.rs, npc_dialogue.rs, forcegreet.rs, dialogue_voice.rs and the Dim 5 systems must be empty (Instant in npc_spawn/resumable/mod.rs/unload.rs is budget/telemetry timing).
  • Written-never-read / tested-but-unwired is the repeated defect class: each gameplay resource needs a production reader (FactionRelations), each pub(crate) entry a non-test caller (#4464 closed: transfer_loot had none while six tests passed; #5432 closed: the audio crate's public stop_sounds_for was tested but uncalled until stop_line_voice wired it, 40b77828b); justify each #[allow(dead_code)].

Phase 3: Merge

Write docs/audits/AUDIT_GAMEPLAY_<TODAY>.md: Executive Summary (severity counts; games/cells exercised); Invariant Matrix (single damage path · death reconciled once · index stability · fail-closed consumables vs fail-open packages · stage order · state saved-or-rederived — verified/drifted); Findings (deduplicated; cross-audit dedup per the split above); Known-Open Register.

Phase 4: Cleanup

rm -rf /tmp/audit/gameplay; tell the user the report is ready and suggest /audit-publish docs/audits/AUDIT_GAMEPLAY_<TODAY>.md (labels gameplay + ai/combat/inventory/quests; game:* only when title-specific).