From 28b273001adac94293ad6c3e0250b9fa29468bdb Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:17:57 +0100 Subject: [PATCH 01/19] fix: restore GlobalOther/GlobalVictim correctly after perception call, 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. --- .../Scripts/Services/Npc/NpcAiService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs index 971fd2c8e..e22244c7a 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs @@ -50,12 +50,12 @@ public void ExecutePerception(VmGothicEnums.PerceptionType type, NpcProperties p } var oldSelf = _gameStateService.GothicVm.GlobalSelf; + var oldOther = _gameStateService.GothicVm.GlobalOther; var oldVictim = _gameStateService.GothicVm.GlobalVictim; - var oldOther = _gameStateService.GothicVm.GlobalVictim; _gameStateService.GothicVm.GlobalSelf = self; - - if(other != null) + + if(other != null) { _gameStateService.GothicVm.GlobalOther = other; } @@ -66,10 +66,10 @@ public void ExecutePerception(VmGothicEnums.PerceptionType type, NpcProperties p } _gameStateService.GothicVm.Call(perceptionFunction); - + _gameStateService.GothicVm.GlobalSelf = oldSelf; + _gameStateService.GothicVm.GlobalOther = oldOther; _gameStateService.GothicVm.GlobalVictim = oldVictim; - _gameStateService.GothicVm.GlobalVictim = oldOther; } public void ExtNpcSetPerceptionTime(NpcInstance npc, float time) @@ -341,8 +341,8 @@ public int ExtNpcGetDistToNpc(NpcInstance npc1, NpcInstance npc2) var npc1Pos = npc1.GetUserData().Go.transform.position; Vector3 npc2Pos; - // If hero - if (npc2.Id == 0) + // If hero: use camera position (VR head position is most accurate) + if (npc2.Index == _gameStateService.GothicVm.GlobalHero?.Index) { npc2Pos = Camera.main!.transform.position; } @@ -395,7 +395,7 @@ public bool ExtNpcIsDead(NpcInstance npcInstance) public bool ExtNpcIsInState(NpcInstance npc, int state) { - return npc.GetUserData().Vob.CurrentStateIndex == state; + return npc.GetUserData()?.Vob.CurrentStateIndex == state; } public bool ExtNpcIsPlayer(NpcInstance npc) From 2e4feff883795d78267cec499f4fc9324715b2eb Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:18:12 +0100 Subject: [PATCH 02/19] fix: NpcHelperService aiState filter compared wrong NPC's state index Filter was comparing npcVob.CurrentStateIndex (the calling NPC's index) against each candidate's state, instead of comparing the requested aiState value. --- Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs index ac0354fe6..b3a41e31f 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs @@ -152,7 +152,7 @@ public bool ExtWldDetectNpcEx(NpcInstance npcInstance, int specificNpcIndex, int _gameStateService.GothicVm.GlobalHero!.Index) // if we don't detect player, then skip it .Where(i => specificNpcIndex < 0 || specificNpcIndex == i.Instance.Index) // Specific NPC is found right now? - .Where(i => aiState < 0 || npcVob.CurrentStateIndex == i.Vob.CurrentStateIndex) + .Where(i => aiState < 0 || aiState == i.Vob.CurrentStateIndex) .Where(i => guild < 0 || i.Instance.Guild == guild) // check guild .OrderBy(i => Vector3.Distance(i.Go.transform.position, npcPos)) // get nearest .FirstOrDefault(); From aa507e7a76b34e7f0a1fe08571299c381ac23528 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:21:13 +0100 Subject: [PATCH 03/19] fix: death/hit animation bypasses queue, guard double-hit on dead NPC, 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 --- .../Npc/Actions/AnimationActions/Attack.cs | 7 +++++++ .../Scripts/Services/Npc/FightService.cs | 20 +++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs index 0c133159d..32da7ff9f 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/Attack.cs @@ -32,6 +32,13 @@ public Attack(AnimationAction action, NpcContainer npcData) : base(action, npcDa public override void Start() { + if (Vob.GuildTrue < (int)VmGothicEnums.Guild.GIL_SEPERATOR_HUM) + { + Logger.Log($"AI_Attack() on human NPC (guild={Vob.GuildTrue}) — not yet implemented, skipping.", LogCat.Ai); + IsFinishedFlag = true; + return; + } + var aiFunctionTemplate = FindAiFunctionTemplate(); _move = VmCacheService.TryGetFightAiData(aiFunctionTemplate, Vob.FightTactic).GetRandomMove(); StartAttackAction(); diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs index 674d5d0a4..163910d20 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs @@ -1,11 +1,9 @@ using Gothic.Core.Adapters.UI.StatusBars; -using Gothic.Core.Domain.Npc.Actions.AnimationActions; using Gothic.Core.Logging; using Gothic.Core.Manager; using Gothic.Core.Models.Container; using Gothic.Core.Models.Vm; using Gothic.Core.Services.Config; -using Gothic.Core.Services.Vm; using Gothic.Core.Services.World; using Reflex.Attributes; using UnityEngine; @@ -17,7 +15,6 @@ namespace Gothic.Core.Services.Npc public class FightService { [Inject] private AudioService _audioService; - [Inject] private VmService _vmService; [Inject] private AnimationService _animationService; [Inject] private PhysicsService _physicsService; [Inject] private NpcHelperService _npcHelperService; @@ -33,6 +30,9 @@ public void Init() private void OnHit(NpcContainer attacker, NpcContainer target, Vector3 __) { + if (target.Props.BodyState == VmGothicEnums.BodyState.BsDead) + return; + Logger.Log($"[FightService.OnHit] *** {attacker.Instance.GetName(NpcNameSlot.Slot0)} HIT {target.Instance.GetName(NpcNameSlot.Slot0)}", LogCat.Npc); if (OnHitUpdateHealth(attacker, target)) { @@ -90,21 +90,21 @@ private bool OnHitUpdateHealth(NpcContainer attacker, NpcContainer target) private void OnDyingChangeAnimation(NpcContainer target) { - // Stop current (attack) animation. - target.Props.CurrentAction.StopImmediately(); + // Clear pending AI queue and stop all running animations (e.g. s_walk still looping). + // Death takes priority over everything — bypass the queue and play directly. + target.Props.AnimationQueue.Clear(); + target.PrefabProps.AnimationSystem.StopAllAnimations(); _physicsService.DisablePhysicsForNpc(target.PrefabProps); var animName = _animationService.GetAnimationName(VmGothicEnums.AnimationType.DeadB, target); - target.Props.AnimationQueue.Enqueue(new PlayAni(new(animName), target)); + target.PrefabProps.AnimationSystem.PlayAnimation(animName); } private void OnHitChangeAnimation(NpcContainer target) { - // Stop current (attack) animation. - target.Props.CurrentAction.StopImmediately(); - + // Play hurt on top of whatever is currently running — don't interrupt the current action. var animName = _animationService.GetAnimationName(VmGothicEnums.AnimationType.StumbleA, target); - target.Props.AnimationQueue.Enqueue(new PlayAni(new(animName), target)); + target.PrefabProps.AnimationSystem.PlayAnimation(animName); } private void OnHitPlaySound(NpcContainer target) From 44129093250d666b3753ad92989c98f43fdab4ea Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:25:19 +0100 Subject: [PATCH 04/19] fix: release FreePoint when NPC switches routine, remove stale commented-out ClearState block --- .../Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index e35458390..c03289dd9 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -254,14 +254,6 @@ public void StartRoutine(int action, string wayPointName) public void StartRoutine(int action) { - // End original loop first - // TODO - Calling ClearState(false) was buggy when e.g. Diego dialog "END" was clicked. Then the dialog lines were skipped. - // if (Properties.CurrentLoopState == NpcProperties.LoopState.Loop) - // { - // // We reuse this function as it is doing what we need. - // ClearState(false); - // } - var didRoutineChange = Vob.CurrentStateIndex != action; Vob.LastAiState = Vob.CurrentStateIndex; @@ -295,6 +287,11 @@ public void StartRoutine(int action) // When we reached end of ZS_*_END, we also call this method. Check if we really altered the routine action or just restarted it. if (didRoutineChange) { + if (Properties.CurrentFreePoint != null) + { + Properties.CurrentFreePoint.IsLocked = false; + Properties.CurrentFreePoint = null; + } Logger.Log($"Start new routine >{routineSymbol.Name}< on >{Go.transform.parent.name}<", LogCat.Ai); Properties.StateTime = 0; } From 9145046dac22ff7a9d9e1e4f73ccbc4b4ef9d5a9 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:26:19 +0100 Subject: [PATCH 05/19] fix: GoToNpc stops 1.5m before target, GoToWp guards null path from FindFastestPath --- .../Domain/Npc/Actions/AnimationActions/GoToNpc.cs | 8 +++++++- .../Domain/Npc/Actions/AnimationActions/GoToWp.cs | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs index 29b970bc6..d2a0e2cc5 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToNpc.cs @@ -6,6 +6,8 @@ namespace Gothic.Core.Domain.Npc.Actions.AnimationActions { public class GoToNpc : AbstractWalkAnimationAction { + private const float ConversationDistance = 1.5f; + private Transform _destinationTransform; public GoToNpc(AnimationAction action, NpcContainer npcContainer) : base(action, npcContainer) @@ -21,7 +23,11 @@ public override void Start() protected override Vector3 GetWalkDestination() { - return _destinationTransform.position; + var targetPos = _destinationTransform.position; + var toTarget = targetPos - NpcGo.transform.position; + if (toTarget.sqrMagnitude < 0.001f) + return targetPos; + return targetPos + toTarget.normalized * -ConversationDistance; } diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs index e889e8861..2cf9ab28e 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/GoToWp.cs @@ -33,8 +33,14 @@ public override void Start() } // We need to set the route now to ensure base.Start() can check if NPC is already _on_ the final destination. - _route = new Stack(WayNetService.FindFastestPath(currentWaypoint.Name, - destinationWaypoint.Name)); + var path = WayNetService.FindFastestPath(currentWaypoint.Name, destinationWaypoint.Name); + if (path == null) + { + IsFinishedFlag = true; + return; + } + + _route = new Stack(path); base.Start(); } From af38def00e9bf46a2b7078ee407c21e351917dab Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:27:44 +0100 Subject: [PATCH 06/19] fix: FormatException when parsing animation event slots with trailing spaces Split(' ') on strings like '5 ' produces an empty token; added Where filter to skip empty entries before Convert.ToInt32 on OptimalFrame, HitEnd, ComboWindow. --- .../Scripts/Adapters/Animations/AnimationSystem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs b/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs index 357f76a9e..3ca3351ec 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Animations/AnimationSystem.cs @@ -565,13 +565,13 @@ private void ApplyEventTags(AnimationTrackInstance trackInstance) AttackAnimation = trackInstance.AnimationName; break; case EventType.OptimalFrame: - AttackOptFrame = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + AttackOptFrame = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList(); break; case EventType.HitEnd: - AttackHitEnd = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + AttackHitEnd = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList(); break; case EventType.ComboWindow: - AttackWindowFrames = eventTag.Slots.Item1.Split(' ').Select(i => Convert.ToInt32(i)).ToList(); + AttackWindowFrames = eventTag.Slots.Item1.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(i => Convert.ToInt32(i)).ToList(); break; // Unused. @see: https://gothic-modding-community.github.io/gmc/zengin/anims/events/#def_dir case EventType.HitDirection: From d172a4f4b2c8941dd7e80dcadbcaa428f43d6572 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:37:54 +0100 Subject: [PATCH 07/19] fix: NpcHeadMeshBuilder null-safe NpcLoader/Npc access during async head build --- .../Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs b/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs index 5b0e7e89d..920fe944e 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Meshes/Builder/NpcHeadMeshBuilder.cs @@ -20,9 +20,14 @@ public override GameObject Build() return RootGo; } - var npcContainer = RootGo.GetComponentInParent().Npc.GetUserData(); + var npcContainer = RootGo.GetComponentInParent()?.Npc?.GetUserData(); + if (npcContainer == null) + { + Logger.LogWarning($"NpcContainer not available during head build for {RootGo.name} — skipping head component setup.", LogCat.Mesh); + return RootGo; + } - // Cache it f1or faster use during runtime + // Cache it for faster use during runtime npcContainer.PrefabProps.Head = headGo.transform; npcContainer.PrefabProps.HeadMorph = headGo.AddComponent().Inject(); npcContainer.PrefabProps.HeadMorph.HeadName = npcContainer.Props.BodyData.Head; From 181d2c6a744da708a8793b644f01f7882e3b1ea8 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 01:39:24 +0100 Subject: [PATCH 08/19] feat: pre-warm TMP font atlas from Daedalus VM string symbols on startup 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. --- .../Scripts/Services/UI/FontService.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs index 327456552..5d4816a30 100644 --- a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs +++ b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using Gothic.Core.Const; using Gothic.Core.Logging; @@ -8,6 +10,7 @@ using TMPro; using UnityEngine; using UnityEngine.TextCore; +using ZenKit; using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Services.UI @@ -30,6 +33,30 @@ public void Create() TMP_Settings.defaultSpriteAsset = DefaultSpriteAsset; TMP_Settings.defaultFontAsset = DefaultFont; + + WarmAtlasFromVm(); + } + + private void WarmAtlasFromVm() + { + if (DefaultFont == null || _gameStateService.GothicVm == null) + return; + + var uniqueChars = new HashSet(); + foreach (var symbol in _gameStateService.GothicVm.Symbols) + { + if (symbol.Type != DaedalusDataType.String || symbol.Size == 0) continue; + for (ushort i = 0; i < symbol.Size; i++) + { + var str = symbol.GetString(i); + if (string.IsNullOrEmpty(str)) continue; + foreach (var c in str) + uniqueChars.Add(c); + } + } + + DefaultFont.TryAddCharacters(new string(uniqueChars.ToArray())); + Logger.Log($"Font atlas pre-warmed with {uniqueChars.Count} unique chars", LogCat.Misc); } [CanBeNull] @@ -109,13 +136,13 @@ public TMP_SpriteAsset TryGetFont(string fontName) return spriteAsset; } - private Material GetDefaultSpriteMaterial(Texture2D spriteSheet = null) + private UnityEngine.Material GetDefaultSpriteMaterial(Texture2D spriteSheet = null) { ShaderUtilities.GetShaderPropertyIDs(); // Add a new material var shader = Constants.ShaderTMPSprite; - var tempMaterial = new Material(shader); + var tempMaterial = new UnityEngine.Material(shader); tempMaterial.SetTexture(ShaderUtilities.ID_MainTex, spriteSheet); return tempMaterial; From bb6a9cc2acb2d35cd345c76639f9109d39420921 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 17:08:48 +0100 Subject: [PATCH 09/19] rollback font --- .../Scripts/Services/UI/FontService.cs | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs index 5d4816a30..6796a4104 100644 --- a/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs +++ b/Assets/Gothic-Core/Scripts/Services/UI/FontService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Gothic.Core.Const; using Gothic.Core.Logging; @@ -10,7 +7,6 @@ using TMPro; using UnityEngine; using UnityEngine.TextCore; -using ZenKit; using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Services.UI @@ -33,30 +29,6 @@ public void Create() TMP_Settings.defaultSpriteAsset = DefaultSpriteAsset; TMP_Settings.defaultFontAsset = DefaultFont; - - WarmAtlasFromVm(); - } - - private void WarmAtlasFromVm() - { - if (DefaultFont == null || _gameStateService.GothicVm == null) - return; - - var uniqueChars = new HashSet(); - foreach (var symbol in _gameStateService.GothicVm.Symbols) - { - if (symbol.Type != DaedalusDataType.String || symbol.Size == 0) continue; - for (ushort i = 0; i < symbol.Size; i++) - { - var str = symbol.GetString(i); - if (string.IsNullOrEmpty(str)) continue; - foreach (var c in str) - uniqueChars.Add(c); - } - } - - DefaultFont.TryAddCharacters(new string(uniqueChars.ToArray())); - Logger.Log($"Font atlas pre-warmed with {uniqueChars.Count} unique chars", LogCat.Misc); } [CanBeNull] @@ -136,13 +108,13 @@ public TMP_SpriteAsset TryGetFont(string fontName) return spriteAsset; } - private UnityEngine.Material GetDefaultSpriteMaterial(Texture2D spriteSheet = null) + private Material GetDefaultSpriteMaterial(Texture2D spriteSheet = null) { ShaderUtilities.GetShaderPropertyIDs(); // Add a new material var shader = Constants.ShaderTMPSprite; - var tempMaterial = new UnityEngine.Material(shader); + var tempMaterial = new Material(shader); tempMaterial.SetTexture(ShaderUtilities.ID_MainTex, spriteSheet); return tempMaterial; From 5941ca50ea4f95999eb01eb1a231fb36c4aa22f8 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 17:41:20 +0100 Subject: [PATCH 10/19] fix for hp bar - they appeared empty in actual VR --- .../Resources/Prefabs/UI/StatusBars/StatusBar.prefab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab b/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab index b7cdcf879..8a7cc1894 100644 --- a/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab +++ b/Assets/Gothic-Core/Resources/Prefabs/UI/StatusBars/StatusBar.prefab @@ -225,7 +225,7 @@ Canvas: m_OverridePixelPerfect: 0 m_SortingBucketNormalizedSize: 0 m_VertexColorAlwaysGammaSpace: 1 - m_AdditionalShaderChannelsFlag: 0 + m_AdditionalShaderChannelsFlag: 25 m_UpdateRectTransformForStandalone: 0 m_SortingLayerID: 0 m_SortingOrder: 0 From 80e70c26ebe83aa84be55c48d490881155fc08b8 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 17:44:54 +0100 Subject: [PATCH 11/19] fix: set LiberationSans SDF empty to Static mode so Gothic bitmap chars 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. --- .../FontAsset/LiberationSans SDF empty.asset | 45 +++++++++---------- .../Prefabs/Player-Elements/BackPack.prefab | 8 ++-- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset b/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset index 858a10bdb..c111263b5 100644 --- a/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset +++ b/Assets/Gothic-Core/Resources/FontAsset/LiberationSans SDF empty.asset @@ -37,6 +37,25 @@ MonoBehaviour: m_TabWidth: 29 m_Material: {fileID: 5152873236376385906} m_SourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75 + m_CreationSettings: + sourceFontFileName: + sourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75 + faceIndex: 0 + pointSizeSamplingMode: 0 + pointSize: 104 + padding: 8 + paddingMode: 1 + packingMode: 0 + atlasWidth: 512 + atlasHeight: 256 + characterSetSelectionMode: 5 + characterSequence: + referencedFontAssetGUID: + referencedTextAssetGUID: + fontStyle: 0 + fontStyleModifier: 0 + renderMode: 4165 + includeFontFeatures: 0 m_SourceFontFile: {fileID: 0} m_SourceFontFilePath: m_AtlasPopulationMode: 0 @@ -67,25 +86,6 @@ MonoBehaviour: m_MarkToMarkAdjustmentRecords: [] m_ShouldReimportFontFeatures: 0 m_FallbackFontAssetTable: [] - m_CreationSettings: - sourceFontFileName: - sourceFontFileGUID: e3265ab4bf004d28a9537516768c1c75 - faceIndex: 0 - pointSizeSamplingMode: 0 - pointSize: 104 - padding: 8 - paddingMode: 1 - packingMode: 0 - atlasWidth: 512 - atlasHeight: 256 - characterSetSelectionMode: 5 - characterSequence: - referencedFontAssetGUID: - referencedTextAssetGUID: - fontStyle: 0 - fontStyleModifier: 0 - renderMode: 4165 - includeFontFeatures: 0 m_FontWeightTable: - regularTypeface: {fileID: 0} italicTypeface: {fileID: 0} @@ -151,17 +151,15 @@ Texture2D: m_ImageContentsHash: serializedVersion: 2 Hash: 00000000000000000000000000000000 - m_ForcedFallbackFormat: 4 - m_DownscaleFallback: 0 m_IsAlphaChannelOptional: 0 - serializedVersion: 2 + serializedVersion: 4 m_Width: 512 m_Height: 256 m_CompleteImageSize: 131072 m_MipsStripped: 0 m_TextureFormat: 1 m_MipCount: 1 - m_IsReadable: 0 + m_IsReadable: 1 m_IsPreProcessed: 0 m_IgnoreMipmapLimit: 0 m_MipmapLimitGroupName: @@ -297,3 +295,4 @@ Material: - _SpecularColor: {r: 1, g: 1, b: 1, a: 1} - _UnderlayColor: {r: 0, g: 0, b: 0, a: 0.5} m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab index b6b86c804..07854330c 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/BackPack.prefab @@ -1293,15 +1293,15 @@ PrefabInstance: m_Modifications: - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_text - value: x/y + value: '' objectReference: {fileID: 0} - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_fontAsset - value: + value: objectReference: {fileID: 11400000, guid: ed34b05229166f3469c21b7208879736, type: 2} - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_sharedMaterial - value: + value: objectReference: {fileID: 5152873236376385906, guid: ed34b05229166f3469c21b7208879736, type: 2} - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_VerticalAlignment @@ -3692,7 +3692,7 @@ PrefabInstance: m_Modifications: - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_text - value: '{{CATEGORY}}' + value: '' objectReference: {fileID: 0} - target: {fileID: 6401121504383089494, guid: 4293d805a850fb84885c55f47b7299bd, type: 3} propertyPath: m_fontSize From 5ac25ed11304ebbda0dd1f0ba9f844e3335ae55a Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Fri, 12 Jun 2026 22:11:40 +0100 Subject: [PATCH 12/19] fix: Npc_HasItems now reads packed inventory; world item pickup amount clamped to 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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.). --- .../Services/Npc/NpcInventoryService.cs | 23 ++++++++++++++----- .../Scripts/Adapters/Player/VRBackpack.cs | 12 +++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs index ccd9edb66..0017f8d19 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcInventoryService.cs @@ -137,15 +137,26 @@ public List GetAllInventoryItems(NpcInstance npc) public int ExtNpcHasItems(NpcInstance npc, int itemId) { - var npcVob = npc.GetUserData()!.Vob; var itemInstanceName = _gameStateService.GothicVm.GetSymbolByIndex(itemId)!.Name; - - for (var i = 0; i < npcVob.ItemCount; i++) + + foreach (InvCats cat in System.Enum.GetValues(typeof(InvCats))) { - if (npcVob.GetItem(i).Name == itemInstanceName) - return npcVob.GetItem(i).Amount; + if (cat == InvCats.InvCatMax) + continue; + try + { + foreach (var item in GetInventoryItems(npc, cat)) + { + if (string.Equals(item.Name, itemInstanceName, System.StringComparison.OrdinalIgnoreCase)) + return item.Amount; + } + } + catch + { + // Category slot was never initialized for this NPC + } } - + return 0; } diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs index 8ad6da4bc..ff31989d0 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs @@ -87,7 +87,7 @@ public void OnItemPutIntoHolster(HVRGrabberBase grabber, HVRGrabbable grabbable) var vobLoader = grabbable.GetComponentInParent(); var vobContainer = vobLoader.Container; - _playerService.AddItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount); + _playerService.AddItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); } @@ -102,8 +102,8 @@ public void OnItemPutIntoBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable _vobMeshCullingService.RemoveCullingEntry(vobContainer); _saveGameService.CurrentWorldData.Vobs.Remove(vobContainer.Vob); - _playerService.AddItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount); - + _playerService.AddItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); + UpdateInventoryView(); } @@ -116,21 +116,21 @@ public void OnItemPutOutOfHolster(HVRGrabberBase grabber, HVRGrabbable grabbable var vobLoader = grabbable.GetComponentInParent(); var vobContainer = vobLoader.Container; - _playerService.RemoveItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount); + _playerService.RemoveItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); } public void OnItemPutOutOfBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable) { if (_tempIgnoreSocketing) return; - + var vobLoader = grabbable.GetComponentInParent(); var vobContainer = vobLoader.Container; _vobMeshCullingService.AddCullingEntry(vobContainer); _saveGameService.CurrentWorldData.Vobs.Add(vobContainer.Vob); - _playerService.RemoveItem(vobContainer.Vob.Name, vobContainer.VobAs().Amount); + _playerService.RemoveItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); UpdateInventoryView(); } From cdff9e9ecc40e66b6cccac50a0ae9a69fec9a07d Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 00:51:20 +0100 Subject: [PATCH 13/19] fix: prevent InitVobCoroutine crash on broken VOBs; fix null GetFirstSound in GetSoundClip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Scripts/Domain/Vobs/VobInitializerDomain.cs | 10 +++++++--- Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs | 7 +++++-- .../Gothic-Core/Scripts/Services/Vobs/VobService.cs | 11 +++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs index 3e37bb0cd..1a1a9110d 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs @@ -581,14 +581,18 @@ public AudioClip GetSoundClip(string soundName) if (sfxContainer == null) return null; + var firstSound = sfxContainer.GetFirstSound(); + if (firstSound == null) + return null; + // Instead of decoding nosound.wav which might be decoded incorrectly, just return null. - if (sfxContainer.GetFirstSound().File.EqualsIgnoreCase(AudioService.NoSoundName)) + if (firstSound.File.EqualsIgnoreCase(AudioService.NoSoundName)) return null; if (sfxContainer.Count > 1) - Logger.LogWarning($"Multiple random elements exist for >{sfxContainer.GetFirstSound().File}< but only first is selected.", LogCat.Audio); + Logger.LogWarning($"Multiple random elements exist for >{firstSound.File}< but only first is selected.", LogCat.Audio); - clip = _audioService.CreateAudioClip(sfxContainer.GetFirstSound().File); + clip = _audioService.CreateAudioClip(firstSound.File); } return clip; diff --git a/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs b/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs index c02327a76..b2285618b 100644 --- a/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs +++ b/Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Gothic.Core.Logging; using Gothic.Core.Services; using Gothic.Core.Const; using Gothic.Core.Extensions; @@ -8,6 +9,7 @@ using MyBox; using Reflex.Attributes; using ZenKit.Daedalus; +using Logger = Gothic.Core.Logging.Logger; namespace Gothic.Core.Models.Audio { @@ -64,9 +66,10 @@ private void LoadSoundEffects() var firstSound = _gameStateService.SfxVm.InitInstance(soundKey); sounds.Add(firstSound); } - catch (Exception e) + catch (Exception) { - // If the key itself doesn't exist, then we don't need to look further. + // SFX symbol missing from Gothic's SFX scripts — expected for some broken VOBs (e.g. Wood_Night2 was never defined). + Logger.LogWarning($"SFX symbol not found in VM: '{soundKey}' — no sound will play.", LogCat.Audio); _soundEffects = Array.Empty(); return; } diff --git a/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs b/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs index 28b608a1e..c6ea15d55 100644 --- a/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Vobs/VobService.cs @@ -176,12 +176,19 @@ private IEnumerator InitVobCoroutine() // } var item = _objectsToInitQueue.Dequeue(); - + item.IsLoaded = true; // We assume that each loaded VOB is centered at parent=0,0,0. // Should work smoothly until we start lazy loading sub-vobs ;-) - _initializerDomain.InitVob(item.Container.Vob, item.gameObject, default, true); + try + { + _initializerDomain.InitVob(item.Container.Vob, item.gameObject, default, true); + } + catch (Exception e) + { + Logger.LogError($"Failed to init VOB {item.name}: {e}", LogCat.Vob); + } yield return _frameSkipperService.TrySkipToNextFrameCoroutine(); } From 9717be15cc540b5a56b5ef62a0de46a872452101 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 00:52:21 +0100 Subject: [PATCH 14/19] fix: release HVR grab and restore kinematic when opening dead NPC loot 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. --- Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs index f0e0e1eac..8664f2289 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/VRNpc.cs @@ -6,6 +6,7 @@ using Gothic.Core.Services; using Gothic.Core.Services.Config; using Gothic.Core.Services.Npc; +using Gothic.Core.Services.World; using Gothic.Core; using Gothic.Core.Extensions; using HurricaneVR.Framework.Core; @@ -22,6 +23,7 @@ public class VRNpc : MonoBehaviour [Inject] private readonly DialogService _dialogService; [Inject] private readonly NpcAiService _npcAiService; [Inject] private readonly ConfigService _configService; + [Inject] private readonly PhysicsService _physicsService; private NpcContainer _npcData; private VRNpcLoot _npcLoot; @@ -39,6 +41,9 @@ public void OnGrabbed(HVRGrabberBase grabber, HVRGrabbable grabbable) if (isDead && _configService.Dev.EnableNpcLooting && _npcLoot != null) { _npcLoot.Toggle(_npcData); + // HVR sets isKinematic=false during grab; release immediately and freeze the corpse + grabber.ForceRelease(); + _physicsService.DisablePhysicsForNpc(_npcData.PrefabProps); return; } From e0f805dc5f90eaaab2370e54ae106b055e9f613f Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 00:57:32 +0100 Subject: [PATCH 15/19] fix: null guards in VobMeshCullingDomain, VRBackpack, AiHandler - 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 --- Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 7 +++++-- .../Scripts/Domain/Culling/VobMeshCullingDomain.cs | 5 +++++ Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index c03289dd9..21af979c0 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -334,8 +334,11 @@ public void ReEnableNpc() var currentRoutine = Properties.RoutineCurrent; if (currentRoutine != null) { - var wpPos = _wayNetService.GetWayNetPoint(currentRoutine.Waypoint).Position; - gameObject.transform.position = _npcService.GetFreeAreaAtSpawnPoint(wpPos); + var wp = _wayNetService.GetWayNetPoint(currentRoutine.Waypoint); + if (wp != null) + gameObject.transform.position = _npcService.GetFreeAreaAtSpawnPoint(wp.Position); + else + Logger.LogWarning($"ReEnableNpc: waypoint '{currentRoutine.Waypoint}' not found for {gameObject.name} — NPC will re-enable at current position.", LogCat.NPC); } // Animation state handling diff --git a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs index 6b94a11a2..c7c726a92 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Culling/VobMeshCullingDomain.cs @@ -572,6 +572,11 @@ private IEnumerator StopVobTrackingBasedOnVelocity() { var key = _pausedVobsToReenable.Keys.ElementAt(i); var rigidBody = _pausedVobsToReenable[key]; + if (rigidBody == null) + { + _pausedVobsToReenable.Remove(key); + continue; + } if (rigidBody.linearVelocity != Vector3.zero) { continue; diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs index ff31989d0..fc5275e31 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs @@ -114,6 +114,8 @@ public void OnItemPutIntoBackpack(HVRGrabberBase grabber, HVRGrabbable grabbable public void OnItemPutOutOfHolster(HVRGrabberBase grabber, HVRGrabbable grabbable) { var vobLoader = grabbable.GetComponentInParent(); + if (vobLoader == null) + return; var vobContainer = vobLoader.Container; _playerService.RemoveItem(vobContainer.Vob.Name, Mathf.Max(1, vobContainer.VobAs().Amount)); From 3bfcc9ca9b38f4db7d88e5c25db42be94829ac9c Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 00:58:12 +0100 Subject: [PATCH 16/19] oops typo --- Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index 21af979c0..23a9d8917 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -338,7 +338,7 @@ public void ReEnableNpc() if (wp != null) gameObject.transform.position = _npcService.GetFreeAreaAtSpawnPoint(wp.Position); else - Logger.LogWarning($"ReEnableNpc: waypoint '{currentRoutine.Waypoint}' not found for {gameObject.name} — NPC will re-enable at current position.", LogCat.NPC); + Logger.LogWarning($"ReEnableNpc: waypoint '{currentRoutine.Waypoint}' not found for {gameObject.name} — NPC will re-enable at current position.", LogCat.Npc); } // Animation state handling From 8682f60d8332a5da04527c67689839f06874e7a5 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 03:12:10 +0100 Subject: [PATCH 17/19] fix: Npc_IsDead now checks BodyState instead of returning false 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. --- Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs index e22244c7a..0adbe470f 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs @@ -388,9 +388,9 @@ public void ExtAiDrawWeapon(NpcInstance npc) public bool ExtNpcIsDead(NpcInstance npcInstance) { - // FIXME - We need to implement it properly. Just fixing NPEs for now! - // FIXME - e.g. used for PC_Thief_AFTERTROLL_Condition() from Daedalus. - return false; + // FIXME - BodyState is runtime-only and lost on NPC reload (e.g. world reload respawns the NPC alive). + // A permanent death flag needs to be persisted in SaveGame state and checked here instead. + return npcInstance.GetUserData()?.Props.BodyState == VmGothicEnums.BodyState.BsDead; } public bool ExtNpcIsInState(NpcInstance npc, int state) From 3c57992fc61c08604d76fb3eed9ab62d2378fa38 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 03:13:04 +0100 Subject: [PATCH 18/19] fix: set GlobalOther=hero before ZS_* loops and dialog condition evaluation After commit 69d7707c 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. --- Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 3 +++ Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index 23a9d8917..9a683c22e 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -79,9 +79,12 @@ private void Update() if (Properties.AnimationQueue.Count == 0) { // We always need to set "self" before executing any Daedalus function. + // "other" defaults to hero here so routine states (ZS_*_Loop) have a sensible fallback. + // Perception calls (ExecutePerception) override GlobalOther themselves with their own save/restore. if (NpcInstance != null) { Vm.GlobalSelf = NpcInstance; + Vm.GlobalOther = Vm.GlobalHero; } DaedalusSymbol loopSymbol; diff --git a/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs b/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs index 63cdc66e0..6635f89e6 100644 --- a/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Player/DialogService.cs @@ -99,9 +99,12 @@ public void StartDialog(NpcContainer npcContainer, bool initialDialogStarting) // TODO - Should be outsourced to some VmManager.Call function which sets and resets values. var oldSelf = _gameStateService.GothicVm.GlobalSelf; + var oldOther = _gameStateService.GothicVm.GlobalOther; _gameStateService.GothicVm.GlobalSelf = npcContainer.Instance; + _gameStateService.GothicVm.GlobalOther = _gameStateService.GothicVm.GlobalHero; var conditionResult = _gameStateService.GothicVm.Call(dialog.Condition); _gameStateService.GothicVm.GlobalSelf = oldSelf; + _gameStateService.GothicVm.GlobalOther = oldOther; // Dialog condition is false if (conditionResult == 0) @@ -146,9 +149,12 @@ private bool TryGetImportant(NpcContainer npcContainer, out InfoInstance item) // TODO - Should be outsourced to some VmManager.Call function which sets and resets values. var oldSelf = _gameStateService.GothicVm.GlobalSelf; + var oldOther = _gameStateService.GothicVm.GlobalOther; _gameStateService.GothicVm.GlobalSelf = npcContainer.Instance; + _gameStateService.GothicVm.GlobalOther = _gameStateService.GothicVm.GlobalHero; var conditionResult = _gameStateService.GothicVm.Call(dialog.Condition); _gameStateService.GothicVm.GlobalSelf = oldSelf; + _gameStateService.GothicVm.GlobalOther = oldOther; if (conditionResult == 0) { From fb8f50f368b43269a19ac3a80ab9a8782a68fcfa Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Sat, 13 Jun 2026 19:21:50 +0100 Subject: [PATCH 19/19] simplest fix is usually the best.... turns out to fix healthbar on PCVR you had to move it forward by 0.02 :) --- Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab index f0a06aff6..3ea4fa907 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCNpc.prefab @@ -764,6 +764,10 @@ PrefabInstance: propertyPath: m_SizeDelta.y value: 2.8 objectReference: {fileID: 0} + - target: {fileID: 2805224566998289421, guid: 6ce7e724ba52cae47a499f87c612cd47, type: 3} + propertyPath: m_LocalPosition.z + value: 0.02 + objectReference: {fileID: 0} - target: {fileID: 8230739814078363531, guid: 6ce7e724ba52cae47a499f87c612cd47, type: 3} propertyPath: m_Pivot.x value: 0.5