From 73cc26a82675c0f96f7c45f29745d235d88422fa Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Mon, 15 Jun 2026 03:57:27 +0100 Subject: [PATCH 1/5] fix: guard npcGo null before transform access in ExtNpcGetDistToWp Returns int.MaxValue early when the NPC GameObject is not loaded, preventing a NullReferenceException that blocked Tpl_1430_CALLSLEEPER condition evaluation during the Chapter 3 ritual. --- .../Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs index b3a41e31f..dde9ff724 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcHelperService.cs @@ -176,17 +176,13 @@ public bool ExtWldDetectNpcEx(NpcInstance npcInstance, int specificNpcIndex, int public int ExtNpcGetDistToWp(NpcInstance npc, string waypointName) { var npcGo = GetNpc(npc); - var npcPos = npcGo.transform.position; - var waypoint = _wayNetService.GetWayNetPoint(waypointName); - if (waypoint == null || !npcGo) - { + if (!npcGo || waypoint == null) return int.MaxValue; - } // *100 as Gothic metrics are in cm, not m. - return (int)(Vector3.Distance(npcPos, waypoint.Position) * 100); + return (int)(Vector3.Distance(npcGo.transform.position, waypoint.Position) * 100); } public int ExtNpcGetTalentSkill(NpcInstance npc, int skillId) From 5da665315f90c5d91e7f6790bb39c6922807f3fe Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Mon, 15 Jun 2026 04:58:47 +0100 Subject: [PATCH 2/5] fix: guard null container/AiHandler in UseMob and ContinueRoutine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UseMob: replace container!.Go null-forgiving with explicit null check so NPCs with no mob within 10m skip gracefully instead of crashing. ContinueRoutine: fall back to GetComponent() when the serialised PrefabProps.AiHandler field is null, and bail out cleanly if still missing, breaking the infinite crash→restart loop. --- .../Npc/Actions/AnimationActions/ContinueRoutine.cs | 11 ++++++++++- .../Domain/Npc/Actions/AnimationActions/UseMob.cs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/ContinueRoutine.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/ContinueRoutine.cs index 81ee465f8..4021de638 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/ContinueRoutine.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/ContinueRoutine.cs @@ -1,3 +1,5 @@ +using Gothic.Core.Adapters.Npc; +using Gothic.Core.Logging; using Gothic.Core.Models.Container; namespace Gothic.Core.Domain.Npc.Actions.AnimationActions @@ -10,7 +12,14 @@ public ContinueRoutine(AnimationAction action, NpcContainer npcContainer) : base public override void Start() { - var ai = PrefabProps.AiHandler; + var ai = PrefabProps.AiHandler ?? NpcGo.GetComponent(); + + if (ai == null) + { + Logger.LogWarning($"[ContinueRoutine] AiHandler null on {NpcGo.name} — skipping routine restart", LogCat.Ai); + IsFinishedFlag = true; + return; + } ai.ClearState(false); diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/UseMob.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/UseMob.cs index 57831ccc5..8e859c44f 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/UseMob.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/UseMob.cs @@ -49,7 +49,7 @@ public override void Start() _mobContainer = container; _mobsiScheme = _mobContainer?.Props.GetVisualScheme(); - if (container!.Go == null) + if (container == null || container.Go == null) { IsFinishedFlag = true; return; From 64a310ea08c71cc7efe769e23770e29e6b939f40 Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Mon, 15 Jun 2026 05:00:17 +0100 Subject: [PATCH 3/5] fix: update CurrentWayPoint/FreePoint in ReEnableNpc after routine exchange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After Npc_ExchangeRoutine (e.g. B_Story_PrepareRitual) the NPC's Props.RoutineCurrent.Waypoint points to the new waypoint, but Props.CurrentWayPoint still held the old pre-exchange waypoint. GoToWp.Start() uses Props.CurrentWayPoint as the path start, so it built a path OLD_WP→NEW_WP with OLD_WP on top of the stack. The NPC was teleported to NEW_WP by ReEnableNpc, immediately "reached" the top-of-stack NEW_WP, then walked backward through the path toward OLD_WP — exactly the regression where Chapter-3 ritual NPCs spawn at the temple and then walk away to their pre-ritual waypoints. Fix: update Props.CurrentWayPoint (and CurrentFreePoint) to the new spawn point inside ReEnableNpc, so GoToWp sees the correct start position and skips the walk entirely. --- Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index 9a683c22e..0b09617cf 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -6,6 +6,7 @@ using Gothic.Core.Extensions; using Gothic.Core.Logging; using Gothic.Core.Models.Vm; +using Gothic.Core.Models.Vob.WayNet; using Gothic.Core.Services; using Gothic.Core.Services.Config; using Gothic.Core.Services.Npc; @@ -339,7 +340,11 @@ public void ReEnableNpc() { var wp = _wayNetService.GetWayNetPoint(currentRoutine.Waypoint); if (wp != null) + { gameObject.transform.position = _npcService.GetFreeAreaAtSpawnPoint(wp.Position); + Properties.CurrentFreePoint = wp as FreePoint; + Properties.CurrentWayPoint = wp as WayPoint; + } else Logger.LogWarning($"ReEnableNpc: waypoint '{currentRoutine.Waypoint}' not found for {gameObject.name} — NPC will re-enable at current position.", LogCat.Npc); } From 4e756d366d7d61e15e219380fbb6689ac2e55e9f Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Mon, 15 Jun 2026 11:08:39 +0100 Subject: [PATCH 4/5] fix: resolve ambiguous WayPoint/FreePoint type aliases in AiHandler --- Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index 0b09617cf..43ea9a0d8 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -6,7 +6,8 @@ using Gothic.Core.Extensions; using Gothic.Core.Logging; using Gothic.Core.Models.Vm; -using Gothic.Core.Models.Vob.WayNet; +using FreePoint = Gothic.Core.Models.Vob.WayNet.FreePoint; +using WayPoint = Gothic.Core.Models.Vob.WayNet.WayPoint; using Gothic.Core.Services; using Gothic.Core.Services.Config; using Gothic.Core.Services.Npc; From e65437524ef5617e3db111b5da8bd8ef5a2b29df Mon Sep 17 00:00:00 2001 From: BoroBongo Date: Mon, 15 Jun 2026 18:24:54 +0100 Subject: [PATCH 5/5] fix: track NPC BodyState during walk and AI state transitions BodyState was not updated when NPCs started or stopped walking, causing Daedalus C_BodyStateContains checks (e.g. in ZS_*_Loop) to see stale values. - AbstractWalkAnimationAction2: set BsWalk/BsRun on StartWalk, BsStand on StopWalk - AiHandler.ClearState: reset to BsStand so interrupted NPCs don't keep walk state - NpcAiService.ExtAiStandUp: reset BsStand immediately (not via queue) so the same Daedalus tick sees the correct value - NpcAiService.ExtAiStartState: when stopCurrentState=true, abandon current state immediately via ClearState and clear stale WayPoint so GoToWp picks the nearest one --- .../Scripts/Adapters/Npc/AiHandler.cs | 1 + .../AbstractWalkAnimationAction2.cs | 6 +++++ .../Scripts/Services/Npc/NpcAiService.cs | 22 +++++++++++++++---- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs index 43ea9a0d8..ae115eb5f 100644 --- a/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs +++ b/Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs @@ -318,6 +318,7 @@ public void ClearState(bool callEndFunction) Properties.AnimationQueue.Clear(); Properties.CurrentAction = new None(new AnimationAction(), NpcData); Properties.CurrentLoopState = NpcProperties.LoopState.None; // i.e. call StartNextState() next frame + Properties.BodyState = VmGothicEnums.BodyState.BsStand; PrefabProps.AnimationSystem.StopAllAnimations(); } diff --git a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/AbstractWalkAnimationAction2.cs b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/AbstractWalkAnimationAction2.cs index 5b5e90322..43578569d 100644 --- a/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/AbstractWalkAnimationAction2.cs +++ b/Assets/Gothic-Core/Scripts/Domain/Npc/Actions/AnimationActions/AbstractWalkAnimationAction2.cs @@ -59,6 +59,11 @@ protected virtual void StartWalk() { PhysicsService.EnablePhysicsForNpc(PrefabProps); + var walkMode = (VmGothicEnums.WalkMode)Vob.AiHuman.WalkMode; + Props.BodyState = walkMode == VmGothicEnums.WalkMode.Walk + ? VmGothicEnums.BodyState.BsWalk + : VmGothicEnums.BodyState.BsRun; + var animName = AnimationService.GetAnimationName(VmGothicEnums.AnimationType.Move, NpcContainer); PrefabProps.AnimationSystem.PlayAnimation(animName); } @@ -66,6 +71,7 @@ protected virtual void StartWalk() protected virtual void StopWalk() { PhysicsService.EnablePhysicsForNpc(PrefabProps); + Props.BodyState = VmGothicEnums.BodyState.BsStand; var animName = AnimationService.GetAnimationName(VmGothicEnums.AnimationType.Move, NpcContainer); PrefabProps.AnimationSystem.StopAnimation(animName); diff --git a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs index 0adbe470f..cc15a4870 100644 --- a/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs +++ b/Assets/Gothic-Core/Scripts/Services/Npc/NpcAiService.cs @@ -207,10 +207,21 @@ public void ExtAiStartState(NpcInstance npc, int action, bool stopCurrentState, { var other = (NpcInstance)_gameStateService.GothicVm.GlobalOther; var victim = (NpcInstance)_gameStateService.GothicVm.GlobalOther; - - npc.GetUserData().Props.AnimationQueue.Enqueue(new StartState( + + var container = npc.GetUserData(); + + if (stopCurrentState) + { + // Abandon current state immediately so the new one starts next frame, not after the whole queue drains. + container.PrefabProps?.AiHandler?.ClearState(false); + container.Props.StateEnd = 0; + container.Props.CurrentWayPoint = null; // forces GoToWp to use nearest WP, not stale pre-interrupt WP + + } + + container.Props.AnimationQueue.Enqueue(new StartState( new AnimationAction(int0: action, bool0: stopCurrentState, string0: wayPointName, instance0: other, instance1: victim), - npc.GetUserData())); + container)); } public void ExtAiLookAt(NpcInstance npc, string wayPointName) @@ -259,7 +270,10 @@ public void ExtAiStandUp(NpcInstance npc) // FIXME - Implement remaining tasks from G1 documentation: // * Ist der Nsc in einem Animatinsstate, wird die passende Rücktransition abgespielt. // * Benutzt der NSC gerade ein MOBSI, poppt er ins stehen. - npc.GetUserData().Props.AnimationQueue.Enqueue(new StandUp(new AnimationAction(), npc.GetUserData())); + var container = npc.GetUserData(); + // Reset immediately (not via queue) so Daedalus C_BodyStateContains checks in the same ZS_*_Loop tick see BsStand. + container.Props.BodyState = VmGothicEnums.BodyState.BsStand; + container.Props.AnimationQueue.Enqueue(new StandUp(new AnimationAction(), container)); } public void ExtAiTurnToNpc(NpcInstance npc, NpcInstance other)