Skip to content

Borobongo/stabilizing v2#367

Merged
BoroBongo merged 19 commits into
releases/stabilizing-for-2026-07from
borobongo/stabilizing-v2
Jun 13, 2026
Merged

Borobongo/stabilizing v2#367
BoroBongo merged 19 commits into
releases/stabilizing-for-2026-07from
borobongo/stabilizing-v2

Conversation

@BoroBongo

@BoroBongo BoroBongo commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Stabilizing v2 — NPC AI, Dialog, Inventory & VR fixes

A second round of stabilization fixes targeting NPC perception globals, dialog conditions, inventory checks, font rendering, and VR-specific edge cases.


Changes by file

LiberationSans SDF empty.asset

Set atlas mode to Static. In Dynamic mode the empty atlas was being populated with Gothic characters on first render, causing them to render in Liberation Sans instead of the Gothic bitmap font. Static prevents any atlas population so all characters fall through to the TMP_SpriteAsset set by AbstractMenu/dialogs.

StatusBar.prefab

Fixed HP bar appearing empty in PCVR headsets.

AnimationSystem.cs

Minor adjustment related to the death/hit animation bypass changes.

AiHandler.cs

  • Set GlobalOther = GlobalHero before executing ZS_* state functions. After the perception save/restore was corrected in NpcAiService, a stale NPC (e.g. a Shadow guard) could remain in GlobalOther between perception calls — causing NPCs to turn their backs during conversations and guild/distance dialog conditions to evaluate against the wrong NPC.
  • Added null guard for waypoint in ReEnableNpc + warning log for bad re-enables.

VobMeshCullingDomain.cs

Skip destroyed Rigidbodys in StopVobTrackingBasedOnVelocity to prevent null-ref exceptions during VOB cleanup.

NpcHeadMeshBuilder.cs

Null-safe NpcLoader/Npc access during async head construction — prevents crash when the NPC is destroyed before the build completes.

Attack.cs

  • Death animation now clears the queue and calls StopAllAnimations before playing, so dying always wins regardless of queued actions.
  • Hurt animation overlays without interrupting the current action.
  • OnHit early-outs if the target is already dead to prevent double damage/animation.
  • AI_Attack is skipped with a log for human NPCs (guild < GIL_SEPERATOR_HUM) — not yet implemented for humans.

GoToNpc.cs

NPCs now stop 1.5 m before the target instead of walking into them.

GoToWp.cs

FindFastestPath returning null no longer crashes — path is guarded before use.

VobInitializerDomain.cs

InitVobCoroutine now wraps each InitVob call in try/catch so a single broken VOB can't halt all lazy loading. Root cause: BIRD/OWL NEAR MARKET referenced wood_night2 which doesn't exist in Gothic's SfxInst.d, causing GetFirstSound() to return null and silently kill the entire coroutine.

SfxModel.cs

Logs a warning when an SFX symbol is missing from the VM instead of silently returning null from GetFirstSound.

FightService.cs

Cooperates with the death animation bypass — HP reaching 0 sets BodyState = BsDead which Npc_IsDead now reads.

NpcAiService.cs

  • GlobalOther/GlobalVictim were incorrectly swapped during restore after perception calls (oldOther was assigned from GlobalVictim instead of GlobalOther), causing both globals to be set to the victim. Fixed save/restore order and replaced the fragile npc2.Id == 0 hero check with an Index comparison against GlobalHero.
  • Npc_IsDead was a FIXME stub hardcoded to return false. Now checks Props.BodyState == BsDead, fixing e.g. Thorus not recognizing Mordrag as dead after being killed.

NpcHelperService.cs

aiState filter was comparing npcVob.CurrentStateIndex (the calling NPC's own state index) against each candidate, instead of comparing the requested aiState value.

NpcInventoryService.cs

ExtNpcHasItems was reading from GetItem/ItemCount (the VOB world-file list), which is never populated by runtime CreateInvItems calls. Rewrote to iterate all InvCats and read from GetPacked via GetInventoryItems — the same path used by all other inventory operations. Fixes Npc_HasItems always returning 0 for the player, breaking dialogs that check inventory (Fisk ore purchase, Bloodwyn extortion, Drax beer, etc.).

DialogService.cs

Save/restore GlobalOther and set it to GlobalHero around Info_*_Condition calls, matching Gothic's convention (self = NPC, other = hero in all dialog condition functions). Prevents important dialogs from failing when a stale NPC was left in GlobalOther.

FontService.cs

Pre-warms the TMP font atlas on startup by iterating all string-type Daedalus symbols, collecting unique characters, and calling TryAddCharacters before any UI renders. Prevents TMP fallback glyphs on first display of Gothic NPC/item names.

VobService.cs

World VOB items have Amount = 0 by Gothic convention (single item = 0). Applied Mathf.Max(1, amount) so items picked up from the floor aren't stored as e.g. ITFOBEERx0 in packed inventory.

BackPack.prefab

Cleared placeholder texts ("x/y", "{{CATEGORY}}") that were triggering TMP warnings before boot because they rendered before FontService set the default sprite asset.

VRBackpack.cs

Added null guard for vobLoader in OnItemPutOutOfHolster. Also applies the Mathf.Max(1, amount) fix for picked-up item amounts.

VRNpc.cs

HVR sets isKinematic = false on the grabbed Rigidbody during a grab, causing the NPC corpse to fall or be pushed by the player capsule after releasing. ForceRelease + DisablePhysicsForNpc are now called when opening the loot panel to keep the body frozen.


How to test

  1. Load the Old Camp and talk to Thorus — he should recognize Mordrag as dead (if killed) and dialog conditions should evaluate against the hero, not a nearby NPC.
  2. Pick up items from the floor and check Npc_HasItems dialogs (Drax beer, Fisk ore) — they should work correctly.
  3. Kill an NPC in VR and open the loot panel — the corpse should not fall or slide around.
  4. Observe NPC routines — NPCs in conversation should face the player, not turn their backs.
  5. Check the HP bar in-headset — should show correct values.

@BoroBongo BoroBongo added the pipeline-test-build Attach to a PR to trigger a build pipeline label Jun 13, 2026
BoroBongo added 18 commits June 13, 2026 15:40
…, use hero Index for distance check

Previously oldOther was incorrectly assigned from GlobalVictim instead of GlobalOther,
causing both globals to be restored to the same (victim) value after every perception call.
Also replaces the fragile npc2.Id==0 hero check with an Index comparison against GlobalHero.
Filter was comparing npcVob.CurrentStateIndex (the calling NPC's index)
against each candidate's state, instead of comparing the requested aiState value.
…, skip AI_Attack for humans

- Death: clear queue + StopAllAnimations then play directly so dying always wins
- Hit: overlay hurt anim without interrupting current action
- OnHit: early-out if target already dead to prevent double damage/animation
- Attack: skip with log for human NPCs (guild < GIL_SEPERATOR_HUM), not yet implemented
… spaces

Split(' ') on strings like '5 ' produces an empty token; added Where filter
to skip empty entries before Convert.ToInt32 on OptimalFrame, HitEnd, ComboWindow.
Iterates all string-type Daedalus symbols and collects unique characters,
then calls TryAddCharacters to bake them into the atlas before any UI renders.
Prevents TMP fallback glyphs on first display of Gothic NPC/item names.
…rs always fall through to sprite asset

TMP's GetTextElement() checks the font asset before the sprite asset. In Dynamic
mode the empty Liberation Sans atlas was being populated with Gothic characters on
first render, causing them to render in Liberation Sans instead of the Gothic bitmap
font. Switching to Static prevents any atlas population so all characters fall
through to the Gothic TMP_SpriteAsset set by AbstractMenu/dialogs.

Also cleared the placeholder texts ("x/y", "{{CATEGORY}}") in BackPack.prefab; they
were triggering TMP warnings before boot because they rendered before FontService set
the default sprite asset.
…t clamped to 1

ExtNpcHasItems was reading from GetItem/ItemCount (VOB world-file list) which is
never populated by runtime CreateInvItems calls. Rewrote to iterate all InvCats
and read from GetPacked via GetInventoryItems — the same path used by all other
inventory operations.

VRBackpack was passing IItem.Amount directly to AddItem/RemoveItem. In ZenKit,
world VOB items have Amount=0 (Gothic convention: single item = amount 0), so
items picked up from the floor were stored as e.g. ITFOBEERx0 in packed inventory.
Applied Mathf.Max(1, amount) to all four socket methods to treat 0 as 1.

Fixes Npc_HasItems always returning 0 for player inventory, breaking dialogs
that check whether the player has items (Fisk ore purchase, Bloodwyn extortion,
Drax beer, etc.).
…Sound in GetSoundClip

InitVobCoroutine now wraps each InitVob call in try/catch so a single broken
VOB can't halt all lazy loading. Root cause was BIRD/OWL NEAR MARKET referencing
wood_night2 which doesn't exist in Gothic's SfxInst.d — GetFirstSound() returned
null and silently killed the coroutine. SfxModel now logs a warning when an SFX
symbol is missing from the VM.
HVR sets isKinematic=false on the grabbed Rigidbody during the grab, which
caused the corpse to be affected by gravity and pushable by the player capsule
after releasing. ForceRelease + DisablePhysicsForNpc ensures the body stays
frozen after the loot panel opens.
- VobMeshCullingDomain: skip destroyed Rigidbodies in StopVobTrackingBasedOnVelocity
- VRBackpack: guard null vobLoader in OnItemPutOutOfHolster
- AiHandler: guard null waypoint in ReEnableNpc, log warning so bad re-enables are visible
Was a FIXME stub hardcoded to return false, preventing any dead-NPC checks from
working (e.g. Thorus couldn't recognize Mordrag as dead after being killed).

Now checks Props.BodyState == BsDead, which FightService sets when HP reaches 0.

Note: BodyState is runtime-only - a world reload will respawn the NPC alive. A
permanent death flag in SaveGame state is needed as a follow-up fix.
…uation

After commit 69d7707 correctly fixed the GlobalOther save/restore in
ExecutePerception, a stale NPC (e.g. a Shadow guard from a recent perception)
remained in GlobalOther between perception calls. This caused two visible bugs:

1. NPCs turned their backs during conversation - routine states like ZS_*_Loop
   called B_AssessTalk/B_SmartTurnToNpc using 'other', which pointed to the stale
   Shadow instead of the player. NPCs would physically rotate toward that guard.

2. Important dialogs refused to trigger - conditions like Npc_GetDistToNpc(self,other)
   or guild attitude checks ran against the Shadow's position/guild, not the hero's,
   causing them to return false and skip the dialog entirely.

Fix in AiHandler: set GlobalOther=GlobalHero before executing ZS_* state functions.
This is just a sensible default - perception calls override GlobalOther via their
own save/restore, so combat/assess perceptions are unaffected.

Fix in DialogService: save/restore GlobalOther and explicitly set it to GlobalHero
around Info_*_Condition calls. In Gothic's convention, self=NPC and other=hero in
all dialog condition functions.
@BoroBongo BoroBongo force-pushed the borobongo/stabilizing-v2 branch from 3a9e778 to 3c57992 Compare June 13, 2026 14:45
@BoroBongo BoroBongo changed the base branch from main to releases/stabilizing-for-2026-07 June 13, 2026 14:45
@BoroBongo BoroBongo removed the pipeline-test-build Attach to a PR to trigger a build pipeline label Jun 13, 2026
@BoroBongo BoroBongo marked this pull request as ready for review June 13, 2026 15:04
@BoroBongo BoroBongo merged commit 08c88a7 into releases/stabilizing-for-2026-07 Jun 13, 2026
@BoroBongo BoroBongo deleted the borobongo/stabilizing-v2 branch June 13, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants