From 064218de71b50b9908fc9e596308ff39d92cbd4f Mon Sep 17 00:00:00 2001 From: junikka Date: Wed, 25 Mar 2026 22:12:33 +0100 Subject: [PATCH] Fix NullReferenceException for pawns with no needs (OWD compatibility) Pawns from mods like "One With Death" can have null needs tracker or null timetable, causing crashes in guard logic. Added null checks for pawn.needs and pawn.timetable across all affected code paths. Co-Authored-By: Claude Opus 4.6 --- Source/1.6/Comps/Comp_Guard.cs | 3 ++- .../DeathSquad/ThinkNode_ConditionalShouldSearchAndKill.cs | 2 +- .../Guardian/ThinkNode_ConditionalNeedPercentageBelow.cs | 4 +++- .../1.6/Guardian/ThinkNode_ConditionalShouldGuardSpot.cs | 2 +- Source/1.6/Patrol/ThinkNode_ConditionalShouldPatrol.cs | 2 +- Source/1.6/Utils.cs | 7 +++++++ 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Source/1.6/Comps/Comp_Guard.cs b/Source/1.6/Comps/Comp_Guard.cs index f30cbfb..d1edea6 100644 --- a/Source/1.6/Comps/Comp_Guard.cs +++ b/Source/1.6/Comps/Comp_Guard.cs @@ -290,7 +290,8 @@ public void ReassureSurroundingPawns() if (!cp.health.capacities.CapableOf(PawnCapacityDefOf.Sight)) continue; - cp.needs.mood.thoughts.memories.TryGainMemory(Utils.SawnGuard, null); + if (cp.needs?.mood?.thoughts?.memories != null) + cp.needs.mood.thoughts.memories.TryGainMemory(Utils.SawnGuard, null); } } } diff --git a/Source/1.6/DeathSquad/ThinkNode_ConditionalShouldSearchAndKill.cs b/Source/1.6/DeathSquad/ThinkNode_ConditionalShouldSearchAndKill.cs index 04e67f3..6549785 100644 --- a/Source/1.6/DeathSquad/ThinkNode_ConditionalShouldSearchAndKill.cs +++ b/Source/1.6/DeathSquad/ThinkNode_ConditionalShouldSearchAndKill.cs @@ -42,7 +42,7 @@ public static bool ShouldSearchAndKill(Pawn pawn) }*/ if (comp == null || !comp.DeathSquadMode() - || (pawn.timetable.CurrentAssignment == TimeAssignmentDefOf.Joy) + || (pawn.timetable != null && pawn.timetable.CurrentAssignment == TimeAssignmentDefOf.Joy) || (pawn.health != null && pawn.health.summaryHealth.SummaryHealthPercent <= 0.55f && !GenAI.EnemyIsNear(pawn, 55f)) || Utils.guardNeedFood(pawn) || Utils.guardNeedJoy(pawn) diff --git a/Source/1.6/Guardian/ThinkNode_ConditionalNeedPercentageBelow.cs b/Source/1.6/Guardian/ThinkNode_ConditionalNeedPercentageBelow.cs index 4cc1415..84699f5 100644 --- a/Source/1.6/Guardian/ThinkNode_ConditionalNeedPercentageBelow.cs +++ b/Source/1.6/Guardian/ThinkNode_ConditionalNeedPercentageBelow.cs @@ -17,7 +17,9 @@ public override ThinkNode DeepCopy(bool resolve = true) protected override bool Satisfied(Pawn pawn) { - return pawn.needs.TryGetNeed(this.need).CurLevelPercentage < this.threshold; + if (pawn.needs == null) return false; + Need need = pawn.needs.TryGetNeed(this.need); + return need != null && need.CurLevelPercentage < this.threshold; } private NeedDef need; diff --git a/Source/1.6/Guardian/ThinkNode_ConditionalShouldGuardSpot.cs b/Source/1.6/Guardian/ThinkNode_ConditionalShouldGuardSpot.cs index 28fac3a..cbc3c2a 100644 --- a/Source/1.6/Guardian/ThinkNode_ConditionalShouldGuardSpot.cs +++ b/Source/1.6/Guardian/ThinkNode_ConditionalShouldGuardSpot.cs @@ -41,7 +41,7 @@ public static bool ShouldGuardSpot(Pawn pawn) Comp_Guard comp = pawn.TryGetComp(); if (comp == null || !comp.GuardMode() - || (pawn.timetable.CurrentAssignment == TimeAssignmentDefOf.Joy) + || (pawn.timetable != null && pawn.timetable.CurrentAssignment == TimeAssignmentDefOf.Joy) || (pawn.health != null && pawn.health.summaryHealth != null && pawn.health.summaryHealth.SummaryHealthPercent <= 0.55f && !GenAI.EnemyIsNear(pawn, 55f)) || ThinkNode_ConditionalShouldSearchAndKill.ShouldSearchAndKill(pawn) || Utils.guardNeedFood(pawn) diff --git a/Source/1.6/Patrol/ThinkNode_ConditionalShouldPatrol.cs b/Source/1.6/Patrol/ThinkNode_ConditionalShouldPatrol.cs index 40b48b3..81783b6 100644 --- a/Source/1.6/Patrol/ThinkNode_ConditionalShouldPatrol.cs +++ b/Source/1.6/Patrol/ThinkNode_ConditionalShouldPatrol.cs @@ -31,7 +31,7 @@ public static bool ShouldPatrol(Pawn pawn) Comp_Guard comp = pawn.TryGetComp(); if (comp == null || comp.affectedPatrol == "" - || (pawn.timetable.CurrentAssignment == TimeAssignmentDefOf.Joy) + || (pawn.timetable != null && pawn.timetable.CurrentAssignment == TimeAssignmentDefOf.Joy) || (pawn.health != null && pawn.health.summaryHealth.SummaryHealthPercent <= 0.55f && !GenAI.EnemyIsNear(pawn, 55f)) || Utils.guardNeedFood(pawn) || Utils.guardNeedJoy(pawn) diff --git a/Source/1.6/Utils.cs b/Source/1.6/Utils.cs index 9d111b0..756d7c8 100644 --- a/Source/1.6/Utils.cs +++ b/Source/1.6/Utils.cs @@ -42,36 +42,43 @@ public static string TranslateTicksToTextIRLSeconds(int ticks) public static bool guardNeedRest(Pawn pawn) { + if (pawn.needs == null) return false; return (pawn.needs.rest != null && pawn.needs.rest.CurLevelPercentage < Settings.minRestStopJob) || (ANDROIDLOADED && chjDroidNeedRest(pawn)); } public static bool guardNeedFood(Pawn pawn) { + if (pawn.needs == null) return false; return (pawn.needs.food != null && pawn.needs.food.CurLevelPercentage < Settings.minFoodStopJob); } public static bool guardNeedJoy(Pawn pawn) { + if (pawn.needs == null) return false; return (pawn.needs.joy != null && pawn.needs.joy.CurLevelPercentage < Settings.minJoyStopJob); } public static bool guardNeedMood(Pawn pawn) { + if (pawn.needs == null) return false; return (pawn.needs.mood != null && pawn.needs.mood.CurLevelPercentage < Settings.minMoodStopJob); } public static bool guardNeedHygiene(Pawn pawn) { + if (pawn.needs == null) return false; return (needHygiene != null && pawn.needs.TryGetNeed(needHygiene) != null && pawn.needs.TryGetNeed(needHygiene).CurLevelPercentage < Settings.minHygieneStopJob); } public static bool guardNeedBladder(Pawn pawn) { + if (pawn.needs == null) return false; return (needBladder != null && pawn.needs.TryGetNeed(needBladder) != null && pawn.needs.TryGetNeed(needBladder).CurLevelPercentage < Settings.minBladderStopJob); } public static bool chjDroidNeedRest(Pawn pawn) { + if (pawn.needs == null) return false; foreach (Need need in pawn.needs.AllNeeds) { if (need != null && need.def.defName == "ChJEnergy")