"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"
Install
npx skillscat add matiaszanolli/byroredux/claude-commands-audit-gameplay Install via the SkillsCat registry.
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
mkdir -p /tmp/audit/gameplay; savegh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labelsto/tmp/audit/gameplay/issues.jsonand dedup every finding against it (_audit-common.md); read the newestdocs/audits/AUDIT_GAMEPLAY_*.md.- 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).InventoryCatalogand the two player templates are read-only, rebuilt byinstall_catalogper content load; a mutable inventory copy in the binary is a boundary violation. - Rows are zeroed in place, never removed (equipment and
EquippedWeapon.inventory_indexpoint intoitems): check consume,transfer_loot,pickup_loot,reference_state::restore. Slot mutations re-deriveEquippedWeapon(reconcile_equipped_weapon); armor toggles must not. ItemInstancehas no fields (_reserved); production allocates only inrestore. Reject claims that per-instance condition/mods/charge exist;Some(instance)paths must still be atomic.- Player body + mid-life gear (P3):
attach_player_bodybuilds a strictly visual body viaNpcSpawnJob(player_bodyflag: no identity/inventory/AI/collider stamps; the capsule stays the only physics body). An equip with noNpcEquipmentPartroot queuesPendingGearImport→GearImportLoader(retainedNpcSkeletonBones/ActorBodyClass);inv.equipmust go throughapply_action. The body comes from theNPC_ 0x7record, 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 restoredEquipmentSlots(reveal / hide / queuePendingGearImport) fromreference_state::restore(NPCs) and the post-apply_deltasplayer block — synthesized in memory, never anEquipmentEventBatch. Mid-life gear is released: a leaves-inventory transfer on a cellless wearer queuesPendingGearRelease→GearImportLoader→cell_loader::unload::release_entities(#5028 f78e018ac; NPC gear stays cell-owned).NpcEquipmentParthas noinventory_index(#5049 dropped it; hide/reveal match onform_id).reconcile_worn_gearbuildsequipped_formsfrom biped slots only, in slot order (the weapon slot never has a mesh, so including it starved the reconcile and mademissingnondeterministic, #5266 6e0594e68); a zero-mask item never reachesoccupants, so a mis-decoded biped mask hides that gear on every restore (Skyrim's pre-BOD2BODTdecodes since #5358, 479414ffe);queue_midlife_importsresolves 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),GearImportLoaderre-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 chainsreconcile_worn_gear_excludingwith 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 guardedcollect_subtree(#5513 9da80470d). The gear-release "still held" filter counts onlycount > 0rows (zeroed rows stay, #5268 fec2d14b2); theadded == falserelease path is forward-latent (transfer_lootsource 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_raceis 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) orpickup_loot. - Locks: key unlock happens on activation, not prompt, and is recorded in
ReferenceLockState(saved).carried_unlock_keyreadsPapyrusPlayerEntity; loot/combat readPlayerEntity(both set inscene.rs; the fly-cam placeholder has noInventory) — can they diverge?lock_levelhas no consumer. - Persistence:
captureruns before unload frees instances;restoreat attach consumes the row;without_parked_stateshields a save reload; parked rows own their payloads; identity is the REFRFormIdPair, 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 onReferenceState(#4465): schema change ⇒FORMAT_MAJORbump (/audit-save). - Leveled loot: containers and NPC CNTO carry lists (#4696) both use
expand_leveled_loot(LVLO counts multiplied, Use-All0x04); only the worn outfit (DOFT/OTFT) keeps the flatexpand_leveled_form_id. A new LVLI call site that takes the flat walk for a carried list is the regression. NpcLootAppearancehides originals only once the replacement is fully staged; never strips a living actor.- NPC dialogue (P4, closed): Late
npc_dialogue_selection_systemreadsActivateEvent(beforeevent_cleanup_system), walks running-questDialRecord::quest_refsbound to the NPC, stampsNpcDialogueTopicintoDialogueRegistry;DialogueSurfaceStatekeys the surface on itsnpcandapply_selectionstrips every other NPC's topic (#5038). Topic reachability follows the CK branch model (#5037, 7ab87c0fb; module doc is the spec): onlyDialogueCategory::Topicwith 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 DIALDATATop-level bit 0x02 (#5224, 305363122: choice-only topics stay link-only). When no owned quest topic qualifies,open_conversationfalls back to the master's generic greeting topic (GREETING on FO3/FNV, DialogueGenericHello on Skyrim; #5367 G, 14cff35ae), resolved once per load order intoEsmIndex::generic_greeting_formand borrowed, not cloned (#5426, 411d0eeaf). FO3/FNV INFOs gate on their OWN quest's running state (QSTI →InfoRecord.quest), ordered byDialogueQuestPriorities(installed once per load; absent = file order) with conditions evaluated against that quest (#5271 dbc07e8f0).select_infocollects 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 seededDialogueRandomState, spoken Say-Once INFOs are disqualified via the savedDialogueSpokenInfoFormsledger 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_MAJOR38 (#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 andforcegreet_open(#5392, 16544d4c9) gate onplayer_can_act, andnpc_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 viaDialogueInfoFragments); 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'sAccessrow declares the fragment queues /SceneActorBindingsas writes (#5307, pinned bynpc_dialogue_selection_declares_everything_the_spoken_fragment_path_acquires). Force-greet (PACK procedure 15, #5367 F):forcegreet_system(Update exclusive) walks aForceGreetDirectivecarrier to the player and opens the package'sPKDDtopic (or the generic greeting) through the same selection viaforcegreet_open. Installers: ambientAmbientBehavior::Dialogue, only for a fully-modeled (no fail-open pass), player-targeted (PTDT0x14or none), Conversation-type (PKDD type 0) package — fail-open had armed 60 FNV bases (#5376, 4e58f241d, guarddialogue_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 unconditionaldialogue.forcegreetconsole door, which also accepts the SkyrimForceGreetprocedure-tree leaf viaPackRecord::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 whileplayer_can_actfails and holds one in radius while the player talks to another NPC (consumed if already talking to the greeter; #5392, guardsdead_player_holds_the_forcegreet_directive,open_conversation_holds_or_consumes_the_forcegreet_directive). Its walk ridesnavmesh_path::cached_walk_step(cached NAVM path, KCCblockedreported: a greeter held against an obstacle forLOCOMOTION_STUCK_REPICK_SECSopens in place, #5452 6316719ed), writesTransformand takesPhysicsWorldonly for the step (#5373/#5371, f0c683ef1;forcegreet_walk_survives_transform_propagation).PackRecord::force_greettakes the caller-resolved PKCU template: a Skyrim concrete package names its behaviour through the template and carries only data-input values, so theForceGreetleaf 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 fromapply_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 throughGlobalFormIdResolver, not the FormID's top byte (#5393; 5b68792b7). Decodes go to the byte-budgetedVoiceSoundCachebehind anAudioWorld::is_activegate (#5382), segments are anchored to the speaker withplay_oneshot_followingand skipped when it has noGlobalTransform(#5410), andstop_line_voice(a 150 ms-fade wrapper overAudioWorld::stop_sounds_for) cuts the line whenapply_selectionreplaces it (every stamped speaker plus the selecting NPC's own prior line, BEFORE the new segments queue), whenend_open_conversationcloses it, and whenreconcile_dead_actorburies 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: onlyDialogueSpokenInfoForms(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→ instantrestoreorTimedRestorations(constant rate, simulation seconds, no overheal banking) →restoration_system(clears onDead/health ≤ 0). restoration_planis all-or-nothing: any unsupported effect, condition, area, delivery, script or AV ⇒None, never a partial potion. Conditions fail-CLOSED (GetHasPerk448/449;IsHardcore586 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;HardcoreModeis 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) vsdocs/engine/charal-*-ruleset.md; Oblivion/FO4+ Aid items are "effects unavailable" by design.Unavailableonly reacheslog::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_systemandnpc_combat_ai_systememitHitEvent; onlycombat_damage_systemwrites Health/Dead.HitEventcleanup is Lateevent_cleanup_system— not a leak. reconcile_dead_actorteardown =clear_ambient_behaviorset +AmbientPackageRuntime+EvaluatePackageRequest+AiCombatState+AnimationPlayer/AnimationStack(actor and skeleton root) + ragdoll; it must grow with each new per-actor runtime component (Dim 5) — checkAmbientEngagement,NpcDialogueTopic,PendingGearImport; it also cuts the dying speaker's voice line (stop_line_voice, #5432).- Combat start:
Effect::StartCombatorfaction_hostility_system(#4414; everyEVALUATION_PERIOD_SECS, range fromDetectionConfigGMSTs + line of sight, noDetectionConfig⇒ no ambient hostility). Ambient combat carriesAmbientEngagementand drops afterDISENGAGE_GRACE_SECSout of contact (#4816); scripted combat never auto-drops —Effect::StartCombatremoves the marker so a quest taking over an ambient fight owns it (#5046, fef67f3c3). Unconscious actors (#5017,ActorControlState.unconscious, mutually exclusive withrestrained): 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_combatmust pause the package while fighting and let it resume (#4703). attack_damage: weapon + CHARAL melee bonus;CreatureAttackonly when unarmed; elseUNARMED_DAMAGE(formulas →/audit-character).MeleeStateis per combatant;PhysicsWorldis taken only between storage passes (#4325). The swing resolves throughcast_ray_corridor(MELEE_SWING_CORRIDOR_RADIUS_BU12 BU ball, #5160) excluding only the aggressor's capsule body — its own bones can be hit, whichresolve_actor_rootturns 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_behaviorat spawn (TPLTUse AI Packageschain, #4093), thenambient_ai_package_system(Update) at most once per in-game minute or onEvaluatePackageRequest, 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 coversGameTimeRes.hourwith passing CTDA (fail-OPEN inpackage_conditions_pass: flag a flip, not the policy). - One behavior per actor by construction (
AmbientBehavior, ten arms incl. Eat/Sleep/Dialogue), but nothing ties itsinsert_at_spawn/insert_at_runtimearms,from_package's procedure chain,clear_ambient_behavior's removals (incl.EatSleepState,ForceGreetDirective,WalkStuckTimer/NavPath) andcrates/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 writingTransform/NavPathitself is the regression, pinned bywalk_movers_share_one_pass_two_write_back, #5580 badc45309);prune_seat_reservationskeeps furniture and claimant liveness. - Eat/Sleep (procedures 3/4,
eat_sleep_system, PostUpdate exclusive): walk once to theEatSleepLocationanchor —NearReferenceref,NearLinkedReference(type 6: the actor's own XLKR target throughPackageTargetRegistry::linked_reference), the spawn-stampedEditorPlacement(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 anANCHOR_RETRY_SECSclock 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 ridesnavmesh_path::cached_walk_stepand consumes the KCCblockedflag (held forLOCOMOTION_STUCK_REPICK_SECS⇒ attempt the seat pick when inside the search radius, #5452 6316719ed); failed seat attempts retry on a coarseSEAT_RETRY_SECSclock over one shared marker gather per frame (#5449 35a009431). Creatures (CreatureActor) are meant never to seat —sandbox_seat_system_innerskips them before any scan andseat_at_markerreturns after the walk — keeping Sandbox's wander/idle and Eat/Sleep's authored location (#5495 9bb8bc475). Verified 2026-10-12: nothing in production insertsCreatureActor(the commit and theregistry_completeness_testsrow sayprepare_creature_statestamps 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 aSeatedactor reruns the seating write path (sit pose, heading snap, marker re-reservation) or dropsSeatedfor the package to re-seat (#5501 b1fed839c). Sleep prefersFurnitureMarkerKind::Sleep, reachable only on Skyrim+ (animation_type == 2): on FO3/FNV the sit fallback IS the sleep path, chairs included (#5390, 9bdfdd1c6, guardlegacy_sleep_actor_seats_at_the_sit_fallback). The walk writesTransformand holdsPhysicsWorldonly around the step (#5373/#5371, f0c683ef1); its batched gather takesTransformfirst like every other mover (#5535 1cb12fb3d). - Locomotion: speed is
WalkSpeed(clip stride, clamp [30,250]) elseLOCOMOTION_WALK_SPEED, combat chase included;advance_stuck_repick(2.5 s) serves only Wander/Patrol — Travel/Follow/Escort/Guard dropblocked(Eat/Sleep and force-greet consume it throughcached_walk_step, #5452).BYRO_NO_AI_LOCOMOTION=1is the only switch; the old per-procedure vars must stay unread. Its guardlocomotion_kill_switch_gates_exactly_the_motion_systemspins the nine in-block movers (incl. the bare-patheat_sleep::eat_sleep_system) and thatforcegreet's Update walk takes the switch viamake_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 statesWalkSpeedand the seated chase (#4710, f002763b4). Both spawn paths innpc_spawn/resumable/(prebaked.rs,runtime.rs) stampWalkAnimation+WalkSpeed, and share the Armor and Skeleton phase units fromresumable/mod.rs(SpawnArmor,spawn_armor_unit,spawn_skeleton_unit; #5312 2f1a76e08 — a fork of either arm's Armor/Skeleton phase is the regression). FO4NPC_template flags resolve through the per-flagTPTAactors (NpcRecord.template_actors, 0 = use theTPLTdefault, #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,pushno-ops without the resource, drained once per frame inapp_frame.rsinto the egui overlay only, withobjectives::drain_transition_notifications(#5153 journal announcements on false→true objective flips; the quest label degrades to the FormID form whenQuestDefinitionRegistryis absent instead of panicking the per-frame drain, #5286 61b4274da,transition_drain_survives_an_absent_definition_registry) spliced ahead — check that a--hudrun without debug-ui still drains and every producer (interaction,inventory,save_io, objectives) reaches it.hud::fractionreadsPlayerEntityonly (#4675) and, like the debug vitals and the SDK projection, composes the ceiling through the oneActorValue::undamaged()(current = undamaged − damage,set_overridelayer included, #5508 627149b62): a new vitals consumer must do both.objectives::ObjectiveHudCacherebuilds only when aQuestRevisionofQuestStageState/QuestObjectiveState/QuestDefinitionRegistrychanges (guardhud_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×Ownernever 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 schedulerdt, #5306 c8c0fe868) whose stage teardown goes throughrelease_entities(#5193; its subtree walk is core's guardedcollect_subtree, #5513) and releases an image cover it replaces (#5206). - Spawn: explicit camera column > nearest door > fallback, with
capsule_overlaps_solidclearance; interiors spawn at the first door's own placement (interior_spawn_point_fix).SetInChargensave refusal →/audit-save;settings_io.rsis a shim overcrates/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 (
AmbientPackageRuntimefirst tick,WalkAnimationfrom the spawn lookup,PickedUpre-stamped byrestore,EatSleepStatere-resolved from the same PLDT anchor andEditorPlacementre-stamped at spawn, #5391). Genuinely unsaved:AiCombatState(sessionEntityIdtarget) — ambient combat is re-derived byfaction_hostility_systemafter load, a scriptedStartCombatagainst a not-otherwise-hostile target is lost. Saved:DialogueSpokenInfoForms,TimedRestorations,HardcoreMode,PersistentReferenceStates,ReferenceLockState,FactionRelations;DeadandActorControlStateare 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 playerFactionRanksreset to the record (#5058), the pre-teardown cinematic purge (#5056)). Snapshot rows whoseFormIdPairthe reload cannot resolve (hysteresis band) are parked intoPersistentReferenceStates(#5054) — a new per-reference fact must reach that park path as well ascapture. - 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, byactivation_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 byscheduler_access_tests::story_dispatch_and_forcegreet_update_ordering_is_pinned, #5415 a9f94455d); PostUpdate movers incl.eat_sleep_systemprecedenpc_walk_animation_system; Latewater_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_actionruns between frames with&mut World. - Determinism:
grep -nE 'rand::|Instant::now|SystemTime'overcombat.rs,inventory.rs,interaction.rs,ai_package.rs,faction_hostility.rs,npc_dialogue.rs,forcegreet.rs,dialogue_voice.rsand the Dim 5 systems must be empty (Instantinnpc_spawn/resumable/mod.rs/unload.rsis budget/telemetry timing). - Written-never-read / tested-but-unwired is the repeated defect class: each gameplay resource needs a production reader (
FactionRelations), eachpub(crate)entry a non-test caller (#4464 closed:transfer_loothad none while six tests passed; #5432 closed: the audio crate's publicstop_sounds_forwas tested but uncalled untilstop_line_voicewired 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).