From 9ea885e8af5c42eda4939808b957e4f7a557a823 Mon Sep 17 00:00:00 2001 From: OLC Date: Wed, 8 Apr 2026 02:49:39 +0800 Subject: [PATCH 1/5] Add custom card outline highlighting and registry system - Introduced ModCardHandOutlinePatchHelper for applying custom highlights to cards in hand. - Created ModCardHandOutlineRegistry to manage outline rules for different card types. - Implemented ModCardHandOutlineRule struct to define outline properties. - Added Harmony patches to NHandCardHolder for updating card outlines and flashing effects based on custom rules. --- Hooks/ModCardHandOutlinePatchHelper.cs | 53 +++++ Hooks/ModCardHandOutlineRegistry.cs | 194 ++++++++++++++++++ Hooks/ModCardHandOutlineRule.cs | 24 +++ .../NHandCardHolderHandOutlinePatches.cs | 31 +++ 4 files changed, 302 insertions(+) create mode 100644 Hooks/ModCardHandOutlinePatchHelper.cs create mode 100644 Hooks/ModCardHandOutlineRegistry.cs create mode 100644 Hooks/ModCardHandOutlineRule.cs create mode 100644 Patches/Cards/NHandCardHolderHandOutlinePatches.cs diff --git a/Hooks/ModCardHandOutlinePatchHelper.cs b/Hooks/ModCardHandOutlinePatchHelper.cs new file mode 100644 index 00000000..22874306 --- /dev/null +++ b/Hooks/ModCardHandOutlinePatchHelper.cs @@ -0,0 +1,53 @@ +using Godot; +using HarmonyLib; +using MegaCrit.Sts2.Core.Combat; +using MegaCrit.Sts2.Core.Models; +using MegaCrit.Sts2.Core.Nodes.Cards.Holders; + +namespace BaseLib.Hooks; + +internal static class ModCardHandOutlinePatchHelper +{ + internal static bool TryGetRule(NHandCardHolder holder, out CardModel model, out ModCardHandOutlineRule rule) + { + model = null!; + rule = default; + + if (!holder.IsNodeReady() || holder.CardNode?.Model is not { } m) + return false; + + var evaluated = ModCardHandOutlineRegistry.EvaluateBest(m); + if (evaluated is not { } r) + return false; + + model = m; + rule = r; + return true; + } + + internal static void ApplyHighlight(NHandCardHolder holder, CardModel model, ModCardHandOutlineRule rule) + { + if (CombatManager.Instance is not { IsInProgress: true }) + return; + + var vanillaShow = model.CanPlay() || model.ShouldGlowRed || model.ShouldGlowGold; + var force = rule.VisibleWhenUnplayable && !vanillaShow; + if (!vanillaShow && !force) + return; + + var highlight = holder.CardNode!.CardHighlight; + if (force) + highlight.AnimShow(); + + highlight.Modulate = rule.Color; + } + + internal static void ApplyFlash(NHandCardHolder holder, ModCardHandOutlineRule rule) + { + if (AccessTools.Field(typeof(NHandCardHolder), "_flash")?.GetValue(holder) is not Control flash || + !GodotObject.IsInstanceValid(flash)) + return; + + flash.Modulate = rule.Color; + } +} \ No newline at end of file diff --git a/Hooks/ModCardHandOutlineRegistry.cs b/Hooks/ModCardHandOutlineRegistry.cs new file mode 100644 index 00000000..01a02e01 --- /dev/null +++ b/Hooks/ModCardHandOutlineRegistry.cs @@ -0,0 +1,194 @@ +using System.Collections.Concurrent; +using Godot; +using MegaCrit.Sts2.Core.Models; +using MegaCrit.Sts2.Core.Nodes.Cards.Holders; + +namespace BaseLib.Hooks; + +/// +/// Per–card-type custom outline colors for the in-hand . +/// Applied after vanilla via Harmony. Foreign providers (e.g. RitsuLib) +/// merge via . +/// +public static class ModCardHandOutlineRegistry +{ + private static readonly Func ForeignPredicateAlreadySatisfied = static _ => true; + + private static int _sequence; + private static int _foreignOrder; + + private static readonly ConcurrentDictionary> RulesByCardType = new(); + private static readonly Lock ForeignLock = new(); + private static readonly List ForeignProviders = []; + + /// + /// Registers a rule for . + /// + public static void Register(ModCardHandOutlineRule rule) where TCard : CardModel + { + Register(typeof(TCard), rule); + } + + /// + /// Registers a rule for (concrete subtype). + /// + public static void Register(Type cardType, ModCardHandOutlineRule rule) + { + ArgumentNullException.ThrowIfNull(cardType); + ArgumentNullException.ThrowIfNull(rule.When); + + if (cardType.IsAbstract || !typeof(CardModel).IsAssignableFrom(cardType)) + throw new ArgumentException( + $"Type '{cardType.FullName}' must be a concrete subtype of {typeof(CardModel).FullName}.", + nameof(cardType)); + + var seq = Interlocked.Increment(ref _sequence); + var wrapped = new RegisteredRule(rule, seq); + + RulesByCardType.AddOrUpdate( + cardType, + _ => [wrapped], + (_, existing) => + { + var copy = new List(existing) { wrapped }; + return copy; + }); + } + + /// + /// Merges outline evaluation from another assembly (e.g. RitsuLib). The delegate must return + /// when no rule applies, otherwise paint fields only — the foreign registry has already + /// evaluated When. Uses so the boundary stays a nullable struct (no heap boxing). + /// + public static void RegisterForeign(string modId, string sourceId, + Func evaluateBestFromForeign) + { + ArgumentException.ThrowIfNullOrWhiteSpace(modId); + ArgumentException.ThrowIfNullOrWhiteSpace(sourceId); + ArgumentNullException.ThrowIfNull(evaluateBestFromForeign); + + var order = Interlocked.Increment(ref _foreignOrder); + lock (ForeignLock) + { + ForeignProviders.Add(new ForeignProvider(evaluateBestFromForeign, order)); + } + } + + /// + /// Clears all rules and foreign providers (tests / tooling). + /// + public static void ClearForTests() + { + RulesByCardType.Clear(); + lock (ForeignLock) + { + ForeignProviders.Clear(); + } + } + + /// + /// Applies the best matching registered outline for this holder. + /// + /// if a rule was applied. + public static bool TryRefreshOutlineForHolder(NHandCardHolder? holder) + { + if (holder == null || !holder.IsNodeReady() || holder.CardNode?.Model is not { } model) + return false; + + var rule = EvaluateBest(model); + if (!rule.HasValue) + return false; + + ModCardHandOutlinePatchHelper.ApplyHighlight(holder, model, rule.Value); + return true; + } + + internal static ModCardHandOutlineRule? EvaluateBest(CardModel model) + { + var local = EvaluateLocalBest(model); + ForeignCandidate? foreignBest = null; + + List snapshot; + lock (ForeignLock) + { + snapshot = [..ForeignProviders]; + } + + foreach (var provider in snapshot) + { + (Color Color, int Priority, bool VisibleWhenUnplayable)? foreignPaint; + try + { + foreignPaint = provider.Evaluate(model); + } + catch + { + continue; + } + + if (foreignPaint is not { } paint) + continue; + + var candidate = new ModCardHandOutlineRule(ForeignPredicateAlreadySatisfied, paint.Color, paint.Priority, + paint.VisibleWhenUnplayable); + + if (foreignBest is null || + RuleWins(candidate, provider.Order, foreignBest.Value.Rule, foreignBest.Value.Order)) + foreignBest = new ForeignCandidate(candidate, provider.Order); + } + + switch (local) + { + case null when foreignBest is null: + return null; + case null: + return foreignBest.Value.Rule; + } + + if (foreignBest is null) + return local.Value.Rule; + + return RuleWins(foreignBest.Value.Rule, foreignBest.Value.Order, local.Value.Rule, local.Value.Sequence) + ? foreignBest.Value.Rule + : local.Value.Rule; + } + + private static RegisteredRule? EvaluateLocalBest(CardModel model) + { + RegisteredRule? best = null; + + for (var t = model.GetType(); + t != null && typeof(CardModel).IsAssignableFrom(t); + t = t.BaseType) + { + if (!RulesByCardType.TryGetValue(t, out var list)) + continue; + + foreach (var entry in list.Where(entry => entry.Rule.When(model)).Where(entry => best is null + || entry.Rule.Priority > best.Value.Rule.Priority + || (entry.Rule.Priority == best.Value.Rule.Priority && + entry.Sequence > best.Value.Sequence))) + best = entry; + } + + return best; + } + + private static bool RuleWins(ModCardHandOutlineRule challenger, int challengerOrder, + ModCardHandOutlineRule incumbent, + int incumbentOrder) + { + if (challenger.Priority != incumbent.Priority) + return challenger.Priority > incumbent.Priority; + + return challengerOrder > incumbentOrder; + } + + private readonly record struct RegisteredRule(ModCardHandOutlineRule Rule, int Sequence); + + private readonly record struct ForeignProvider( + Func Evaluate, + int Order); + + private readonly record struct ForeignCandidate(ModCardHandOutlineRule Rule, int Order); +} \ No newline at end of file diff --git a/Hooks/ModCardHandOutlineRule.cs b/Hooks/ModCardHandOutlineRule.cs new file mode 100644 index 00000000..0595ef43 --- /dev/null +++ b/Hooks/ModCardHandOutlineRule.cs @@ -0,0 +1,24 @@ +using Godot; +using MegaCrit.Sts2.Core.Models; + +namespace BaseLib.Hooks; + +/// +/// Custom hand-card outline tint for after vanilla +/// playable / gold / red. Register with . +/// +/// When this returns true for the card instance, the outline color may apply. +/// Godot modulate color (alpha is respected; vanilla highlights use ~0.98). +/// +/// When several rules match, the highest wins; ties favor the most recently registered +/// rule. +/// +/// +/// If true, the highlight is forced visible with this color even when the card is not playable and vanilla would not +/// show gold/red (still only while combat is in progress). +/// +public readonly record struct ModCardHandOutlineRule( + Func When, + Color Color, + int Priority = 0, + bool VisibleWhenUnplayable = false); \ No newline at end of file diff --git a/Patches/Cards/NHandCardHolderHandOutlinePatches.cs b/Patches/Cards/NHandCardHolderHandOutlinePatches.cs new file mode 100644 index 00000000..c13dc30e --- /dev/null +++ b/Patches/Cards/NHandCardHolderHandOutlinePatches.cs @@ -0,0 +1,31 @@ +using BaseLib.Hooks; +using HarmonyLib; +using MegaCrit.Sts2.Core.Nodes.Cards.Holders; + +namespace BaseLib.Patches.Cards; + +[HarmonyPatch(typeof(NHandCardHolder), nameof(NHandCardHolder.UpdateCard))] +internal static class NHandCardHolderUpdateCardHandOutlinePatch +{ + [HarmonyPostfix] + public static void Postfix(NHandCardHolder __instance) + { + if (!ModCardHandOutlinePatchHelper.TryGetRule(__instance, out var model, out var rule)) + return; + + ModCardHandOutlinePatchHelper.ApplyHighlight(__instance, model, rule); + } +} + +[HarmonyPatch(typeof(NHandCardHolder), nameof(NHandCardHolder.Flash))] +internal static class NHandCardHolderFlashHandOutlinePatch +{ + [HarmonyPostfix] + public static void Postfix(NHandCardHolder __instance) + { + if (!ModCardHandOutlinePatchHelper.TryGetRule(__instance, out _, out var rule)) + return; + + ModCardHandOutlinePatchHelper.ApplyFlash(__instance, rule); + } +} \ No newline at end of file From ec6c4b76219345333f02859e0053c87a0ef20aa6 Mon Sep 17 00:00:00 2001 From: OLC Date: Fri, 24 Apr 2026 17:55:23 +0800 Subject: [PATCH 2/5] Enhance dynamic outline functionality for card holders - Updated ModCardHandOutlinePatchHelper to utilize dynamic color resolution for highlights. - Modified ModCardHandOutlineRule to include a dynamic color resolver. - Implemented TryRefreshDynamicOutlineForHolder method in ModCardHandOutlineRegistry for conditional outline application. - Added Harmony patches to NHandCardHolder for dynamic outline updates during the card lifecycle. --- Hooks/ModCardHandOutlinePatchHelper.cs | 6 +- Hooks/ModCardHandOutlineRegistry.cs | 16 +++++ Hooks/ModCardHandOutlineRule.cs | 31 +++++++++- .../NHandCardHolderHandOutlinePatches.cs | 59 ++++++++++++++++++- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/Hooks/ModCardHandOutlinePatchHelper.cs b/Hooks/ModCardHandOutlinePatchHelper.cs index 22874306..1d5e5b90 100644 --- a/Hooks/ModCardHandOutlinePatchHelper.cs +++ b/Hooks/ModCardHandOutlinePatchHelper.cs @@ -39,15 +39,15 @@ internal static void ApplyHighlight(NHandCardHolder holder, CardModel model, Mod if (force) highlight.AnimShow(); - highlight.Modulate = rule.Color; + highlight.Modulate = rule.ResolveColor(model); } - internal static void ApplyFlash(NHandCardHolder holder, ModCardHandOutlineRule rule) + internal static void ApplyFlash(NHandCardHolder holder, CardModel model, ModCardHandOutlineRule rule) { if (AccessTools.Field(typeof(NHandCardHolder), "_flash")?.GetValue(holder) is not Control flash || !GodotObject.IsInstanceValid(flash)) return; - flash.Modulate = rule.Color; + flash.Modulate = rule.ResolveColor(model); } } \ No newline at end of file diff --git a/Hooks/ModCardHandOutlineRegistry.cs b/Hooks/ModCardHandOutlineRegistry.cs index 01a02e01..dd316115 100644 --- a/Hooks/ModCardHandOutlineRegistry.cs +++ b/Hooks/ModCardHandOutlineRegistry.cs @@ -103,6 +103,22 @@ public static bool TryRefreshOutlineForHolder(NHandCardHolder? holder) return true; } + /// + /// Applies outline only when the winning rule uses . + /// + public static bool TryRefreshDynamicOutlineForHolder(NHandCardHolder? holder) + { + if (holder == null || !holder.IsNodeReady() || holder.CardNode?.Model is not { } model) + return false; + + var rule = EvaluateBest(model); + if (!rule.HasValue || rule.Value.DynamicColor == null) + return false; + + ModCardHandOutlinePatchHelper.ApplyHighlight(holder, model, rule.Value); + return true; + } + internal static ModCardHandOutlineRule? EvaluateBest(CardModel model) { var local = EvaluateLocalBest(model); diff --git a/Hooks/ModCardHandOutlineRule.cs b/Hooks/ModCardHandOutlineRule.cs index 0595ef43..66046a5a 100644 --- a/Hooks/ModCardHandOutlineRule.cs +++ b/Hooks/ModCardHandOutlineRule.cs @@ -21,4 +21,33 @@ public readonly record struct ModCardHandOutlineRule( Func When, Color Color, int Priority = 0, - bool VisibleWhenUnplayable = false); \ No newline at end of file + bool VisibleWhenUnplayable = false) +{ + /// + /// Optional dynamic color resolver. When assigned and passes, this is evaluated on refresh + /// to produce current outline color. + /// + public Func? DynamicColor { get; init; } + + /// + /// Creates a rule with a dynamic color resolver. + /// + public static ModCardHandOutlineRule Dynamic( + Func when, + Func colorWhen, + int priority = 0, + bool visibleWhenUnplayable = false) + { + ArgumentNullException.ThrowIfNull(when); + ArgumentNullException.ThrowIfNull(colorWhen); + return new ModCardHandOutlineRule(when, Colors.White, priority, visibleWhenUnplayable) + { + DynamicColor = colorWhen, + }; + } + + internal Color ResolveColor(CardModel card) + { + return DynamicColor?.Invoke(card) ?? Color; + } +} \ No newline at end of file diff --git a/Patches/Cards/NHandCardHolderHandOutlinePatches.cs b/Patches/Cards/NHandCardHolderHandOutlinePatches.cs index c13dc30e..9174e877 100644 --- a/Patches/Cards/NHandCardHolderHandOutlinePatches.cs +++ b/Patches/Cards/NHandCardHolderHandOutlinePatches.cs @@ -1,5 +1,8 @@ +using System.Collections.Concurrent; using BaseLib.Hooks; +using Godot; using HarmonyLib; +using MegaCrit.Sts2.Core.Helpers; using MegaCrit.Sts2.Core.Nodes.Cards.Holders; namespace BaseLib.Patches.Cards; @@ -23,9 +26,61 @@ internal static class NHandCardHolderFlashHandOutlinePatch [HarmonyPostfix] public static void Postfix(NHandCardHolder __instance) { - if (!ModCardHandOutlinePatchHelper.TryGetRule(__instance, out _, out var rule)) + if (!ModCardHandOutlinePatchHelper.TryGetRule(__instance, out var model, out var rule)) + return; + + ModCardHandOutlinePatchHelper.ApplyFlash(__instance, model, rule); + } +} + +[HarmonyPatch(typeof(NHandCardHolder), nameof(NHandCardHolder._Ready))] +internal static class NHandCardHolderDynamicOutlineReadyPatch +{ + private static readonly ConcurrentDictionary TokensByHolderId = new(); + + [HarmonyPostfix] + public static void Postfix(NHandCardHolder __instance) + { + var id = __instance.GetInstanceId(); + if (!TokensByHolderId.TryAdd(id, new CancellationTokenSource())) + return; + + var cts = TokensByHolderId[id]; + TaskHelper.RunSafely(RunDynamicRefreshLoop(__instance, id, cts.Token)); + } + + private static async Task RunDynamicRefreshLoop(NHandCardHolder holder, ulong id, CancellationToken token) + { + try + { + while (!token.IsCancellationRequested && GodotObject.IsInstanceValid(holder)) + { + ModCardHandOutlineRegistry.TryRefreshDynamicOutlineForHolder(holder); + await holder.ToSignal(holder.GetTree(), SceneTree.SignalName.ProcessFrame); + } + } + finally + { + StopLoop(id); + } + } + + internal static void StopLoop(ulong id) + { + if (!TokensByHolderId.TryRemove(id, out var cts)) return; - ModCardHandOutlinePatchHelper.ApplyFlash(__instance, rule); + cts.Cancel(); + cts.Dispose(); + } +} + +[HarmonyPatch(typeof(NHandCardHolder), nameof(NHandCardHolder._ExitTree))] +internal static class NHandCardHolderDynamicOutlineExitTreePatch +{ + [HarmonyPrefix] + public static void Prefix(NHandCardHolder __instance) + { + NHandCardHolderDynamicOutlineReadyPatch.StopLoop(__instance.GetInstanceId()); } } \ No newline at end of file From fa52495bc04d2cd69d1fd7907c57e393b97d8521 Mon Sep 17 00:00:00 2001 From: OLC Date: Fri, 24 Apr 2026 17:57:23 +0800 Subject: [PATCH 3/5] Add support for foreign dynamic outline evaluation - Introduced ForeignDynamicProvider to handle dynamic outline evaluations from external assemblies. - Implemented RegisterForeignDynamic method for registering dynamic color resolvers. - Updated outline merging logic to incorporate dynamic evaluations alongside existing rules. - Cleared dynamic providers during the reset process to ensure clean state management. --- Hooks/ModCardHandOutlineRegistry.cs | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/Hooks/ModCardHandOutlineRegistry.cs b/Hooks/ModCardHandOutlineRegistry.cs index dd316115..3916a45c 100644 --- a/Hooks/ModCardHandOutlineRegistry.cs +++ b/Hooks/ModCardHandOutlineRegistry.cs @@ -20,6 +20,7 @@ public static class ModCardHandOutlineRegistry private static readonly ConcurrentDictionary> RulesByCardType = new(); private static readonly Lock ForeignLock = new(); private static readonly List ForeignProviders = []; + private static readonly List ForeignDynamicProviders = []; /// /// Registers a rule for . @@ -74,6 +75,23 @@ public static void RegisterForeign(string modId, string sourceId, } } + /// + /// Merges dynamic outline evaluation from another assembly. The delegate returns current paint resolver and metadata. + /// + public static void RegisterForeignDynamic(string modId, string sourceId, + Func ResolveColor, int Priority, bool VisibleWhenUnplayable)?> evaluateBestFromForeign) + { + ArgumentException.ThrowIfNullOrWhiteSpace(modId); + ArgumentException.ThrowIfNullOrWhiteSpace(sourceId); + ArgumentNullException.ThrowIfNull(evaluateBestFromForeign); + + var order = Interlocked.Increment(ref _foreignOrder); + lock (ForeignLock) + { + ForeignDynamicProviders.Add(new ForeignDynamicProvider(evaluateBestFromForeign, order)); + } + } + /// /// Clears all rules and foreign providers (tests / tooling). /// @@ -83,6 +101,7 @@ public static void ClearForTests() lock (ForeignLock) { ForeignProviders.Clear(); + ForeignDynamicProviders.Clear(); } } @@ -153,6 +172,41 @@ public static bool TryRefreshDynamicOutlineForHolder(NHandCardHolder? holder) foreignBest = new ForeignCandidate(candidate, provider.Order); } + List dynamicSnapshot; + lock (ForeignLock) + { + dynamicSnapshot = [..ForeignDynamicProviders]; + } + + foreach (var provider in dynamicSnapshot) + { + (Func ResolveColor, int Priority, bool VisibleWhenUnplayable)? foreignPaint; + try + { + foreignPaint = provider.Evaluate(model); + } + catch + { + continue; + } + + if (foreignPaint is not { } paint || paint.ResolveColor == null) + continue; + + var candidate = new ModCardHandOutlineRule( + ForeignPredicateAlreadySatisfied, + paint.ResolveColor(), + paint.Priority, + paint.VisibleWhenUnplayable) + { + DynamicColor = _ => paint.ResolveColor(), + }; + + if (foreignBest is null || + RuleWins(candidate, provider.Order, foreignBest.Value.Rule, foreignBest.Value.Order)) + foreignBest = new ForeignCandidate(candidate, provider.Order); + } + switch (local) { case null when foreignBest is null: @@ -206,5 +260,9 @@ private readonly record struct ForeignProvider( Func Evaluate, int Order); + private readonly record struct ForeignDynamicProvider( + Func ResolveColor, int Priority, bool VisibleWhenUnplayable)?> Evaluate, + int Order); + private readonly record struct ForeignCandidate(ModCardHandOutlineRule Rule, int Order); } \ No newline at end of file From b576ece640382f47dfb8e5e091337d99ad99b31b Mon Sep 17 00:00:00 2001 From: OLC Date: Fri, 24 Apr 2026 22:33:43 +0800 Subject: [PATCH 4/5] Refactor card type registration validation in ModCardHandOutlineRegistry - Updated the registration method to simplify the check for concrete subtypes of CardModel. - Improved documentation for clarity on the registration process. --- Hooks/ModCardHandOutlineRegistry.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Hooks/ModCardHandOutlineRegistry.cs b/Hooks/ModCardHandOutlineRegistry.cs index 3916a45c..c8b203bd 100644 --- a/Hooks/ModCardHandOutlineRegistry.cs +++ b/Hooks/ModCardHandOutlineRegistry.cs @@ -31,16 +31,16 @@ public static void Register(ModCardHandOutlineRule rule) where TCard : Ca } /// - /// Registers a rule for (concrete subtype). + /// Registers a rule for ( subtype). /// public static void Register(Type cardType, ModCardHandOutlineRule rule) { ArgumentNullException.ThrowIfNull(cardType); ArgumentNullException.ThrowIfNull(rule.When); - if (cardType.IsAbstract || !typeof(CardModel).IsAssignableFrom(cardType)) + if (!typeof(CardModel).IsAssignableFrom(cardType)) throw new ArgumentException( - $"Type '{cardType.FullName}' must be a concrete subtype of {typeof(CardModel).FullName}.", + $"Type '{cardType.FullName}' must be a subtype of {typeof(CardModel).FullName}.", nameof(cardType)); var seq = Interlocked.Increment(ref _sequence); From fd28c4ea1391c3ee3db1c13845835def81c0c954 Mon Sep 17 00:00:00 2001 From: OLC Date: Sun, 26 Apr 2026 01:09:27 +0800 Subject: [PATCH 5/5] Improve instance validation in NHandCardHolder dynamic outline patch - Added checks to ensure the NHandCardHolder instance is valid and inside the scene tree before proceeding with dynamic outline updates. - Enhanced cancellation logic to prevent unnecessary processing when the holder is not valid or not in the tree. --- Patches/Cards/NHandCardHolderHandOutlinePatches.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Patches/Cards/NHandCardHolderHandOutlinePatches.cs b/Patches/Cards/NHandCardHolderHandOutlinePatches.cs index 9174e877..739de2a0 100644 --- a/Patches/Cards/NHandCardHolderHandOutlinePatches.cs +++ b/Patches/Cards/NHandCardHolderHandOutlinePatches.cs @@ -41,6 +41,9 @@ internal static class NHandCardHolderDynamicOutlineReadyPatch [HarmonyPostfix] public static void Postfix(NHandCardHolder __instance) { + if (!GodotObject.IsInstanceValid(__instance) || !__instance.IsInsideTree() || __instance.GetTree() == null) + return; + var id = __instance.GetInstanceId(); if (!TokensByHolderId.TryAdd(id, new CancellationTokenSource())) return; @@ -55,8 +58,15 @@ private static async Task RunDynamicRefreshLoop(NHandCardHolder holder, ulong id { while (!token.IsCancellationRequested && GodotObject.IsInstanceValid(holder)) { + if (!holder.IsInsideTree()) + break; + ModCardHandOutlineRegistry.TryRefreshDynamicOutlineForHolder(holder); - await holder.ToSignal(holder.GetTree(), SceneTree.SignalName.ProcessFrame); + var tree = holder.GetTree(); + if (tree == null || !GodotObject.IsInstanceValid(tree)) + break; + + await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame); } } finally