Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
28b2730
fix: restore GlobalOther/GlobalVictim correctly after perception call…
BoroBongo Jun 12, 2026
2e4feff
fix: NpcHelperService aiState filter compared wrong NPC's state index
BoroBongo Jun 12, 2026
aa507e7
fix: death/hit animation bypasses queue, guard double-hit on dead NPC…
BoroBongo Jun 12, 2026
4412909
fix: release FreePoint when NPC switches routine, remove stale commen…
BoroBongo Jun 12, 2026
9145046
fix: GoToNpc stops 1.5m before target, GoToWp guards null path from F…
BoroBongo Jun 12, 2026
af38def
fix: FormatException when parsing animation event slots with trailing…
BoroBongo Jun 12, 2026
d172a4f
fix: NpcHeadMeshBuilder null-safe NpcLoader/Npc access during async h…
BoroBongo Jun 12, 2026
181d2c6
feat: pre-warm TMP font atlas from Daedalus VM string symbols on startup
BoroBongo Jun 12, 2026
bb6a9cc
rollback font
BoroBongo Jun 12, 2026
5941ca5
fix for hp bar - they appeared empty in actual VR
BoroBongo Jun 12, 2026
80e70c2
fix: set LiberationSans SDF empty to Static mode so Gothic bitmap cha…
BoroBongo Jun 12, 2026
5ac25ed
fix: Npc_HasItems now reads packed inventory; world item pickup amoun…
BoroBongo Jun 12, 2026
cdff9e9
fix: prevent InitVobCoroutine crash on broken VOBs; fix null GetFirst…
BoroBongo Jun 12, 2026
9717be1
fix: release HVR grab and restore kinematic when opening dead NPC loot
BoroBongo Jun 12, 2026
e0f805d
fix: null guards in VobMeshCullingDomain, VRBackpack, AiHandler
BoroBongo Jun 12, 2026
3bfcc9c
oops typo
BoroBongo Jun 12, 2026
8682f60
fix: Npc_IsDead now checks BodyState instead of returning false
BoroBongo Jun 13, 2026
3c57992
fix: set GlobalOther=hero before ZS_* loops and dialog condition eval…
BoroBongo Jun 13, 2026
fb8f50f
simplest fix is usually the best.... turns out to fix healthbar on PC…
BoroBongo Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 13 additions & 10 deletions Assets/Gothic-Core/Scripts/Adapters/Npc/AiHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -254,14 +257,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;
Expand Down Expand Up @@ -295,6 +290,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;
}
Expand Down Expand Up @@ -337,8 +337,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ public override GameObject Build()
return RootGo;
}

var npcContainer = RootGo.GetComponentInParent<NpcLoader>().Npc.GetUserData();
var npcContainer = RootGo.GetComponentInParent<NpcLoader>()?.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<HeadMorph>().Inject();
npcContainer.PrefabProps.HeadMorph.HeadName = npcContainer.Props.BodyData.Head;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DijkstraWaypoint>(WayNetService.FindFastestPath(currentWaypoint.Name,
destinationWaypoint.Name));
var path = WayNetService.FindFastestPath(currentWaypoint.Name, destinationWaypoint.Name);
if (path == null)
{
IsFinishedFlag = true;
return;
}

_route = new Stack<DijkstraWaypoint>(path);

base.Start();
}
Expand Down
10 changes: 7 additions & 3 deletions Assets/Gothic-Core/Scripts/Domain/Vobs/VobInitializerDomain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions Assets/Gothic-Core/Scripts/Models/Audio/SfxModel.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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;
using JetBrains.Annotations;
using MyBox;
using Reflex.Attributes;
using ZenKit.Daedalus;
using Logger = Gothic.Core.Logging.Logger;

namespace Gothic.Core.Models.Audio
{
Expand Down Expand Up @@ -64,9 +66,10 @@ private void LoadSoundEffects()
var firstSound = _gameStateService.SfxVm.InitInstance<SoundEffectInstance>(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<SoundEffectInstance>();
return;
}
Expand Down
20 changes: 10 additions & 10 deletions Assets/Gothic-Core/Scripts/Services/Npc/FightService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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))
{
Expand Down Expand Up @@ -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)
Expand Down
Loading