diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fb71105 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Unit Tests + +on: + push: + branches: [main, master] + paths: + - 'Source/**' + - 'Tests/**' + - 'ArchTests/**' + - '.github/workflows/test.yml' + pull_request: + branches: [main, master] + paths: + - 'Source/**' + - 'Tests/**' + - 'ArchTests/**' + - '.github/workflows/test.yml' + +jobs: + test: + uses: RimWorld-RimMind-Mod/Workflows/.github/workflows/test.yml@main + secrets: inherit diff --git a/Source/Bridge/BridgeModuleCoordinator.cs b/Source/Bridge/BridgeModuleCoordinator.cs index ac81a81..ac4a385 100644 --- a/Source/Bridge/BridgeModuleCoordinator.cs +++ b/Source/Bridge/BridgeModuleCoordinator.cs @@ -1,15 +1,13 @@ using System; using System.Collections.Generic; using RimMind.Application.Common.Interfaces.Extension; +using RimMind.Application.Features.Bridges; namespace RimMind.Bridge.RimTalk.Bridge { internal sealed class BridgeModuleCoordinator { - private readonly IReadOnlyList _modules; - private readonly Func _isDependencyActive; - private readonly Action _log; - private readonly Action _warn; + private readonly ExternalModBridgeCoordinator _inner; public BridgeModuleCoordinator( IReadOnlyList modules, @@ -17,48 +15,26 @@ public BridgeModuleCoordinator( Action log, Action warn) { - _modules = modules ?? throw new ArgumentNullException(nameof(modules)); - _isDependencyActive = isDependencyActive - ?? throw new ArgumentNullException(nameof(isDependencyActive)); - _log = log ?? throw new ArgumentNullException(nameof(log)); - _warn = warn ?? throw new ArgumentNullException(nameof(warn)); + if (modules == null) throw new ArgumentNullException(nameof(modules)); + if (isDependencyActive == null) throw new ArgumentNullException(nameof(isDependencyActive)); + if (log == null) throw new ArgumentNullException(nameof(log)); + if (warn == null) throw new ArgumentNullException(nameof(warn)); + + _inner = new ExternalModBridgeCoordinator( + isDependencyActive, + modules, + gates: null, + logInfo: log, + logWarn: warn, + inactiveMessage: "RimTalk not active, bridge modules skipped."); } - public void RegisterAll() - { - if (!_isDependencyActive()) - { - _log("RimTalk not active, bridge modules skipped."); - return; - } + public bool AreAllRegistered => _inner.AreAllRegistered; - foreach (IBridgeModule module in _modules) - { - try - { - module.Register(); - _log($"{module.Id} registered={module.IsRegistered}."); - } - catch (Exception ex) - { - _warn($"{module.Id} registration failed: {ex.Message}"); - } - } - } + public void RegisterAll() => _inner.RegisterAll(); - public void UnregisterAll() - { - foreach (IBridgeModule module in _modules) - { - try - { - module.Unregister(); - } - catch (Exception ex) - { - _warn($"{module.Id} cleanup failed: {ex.Message}"); - } - } - } + public void UnregisterAll() => _inner.UnregisterAll(); + + public void Refresh() => _inner.Refresh(); } } diff --git a/Source/Bridge/ContextPullBridge.cs b/Source/Bridge/ContextPullBridge.cs index 9962d7b..34871c0 100644 --- a/Source/Bridge/ContextPullBridge.cs +++ b/Source/Bridge/ContextPullBridge.cs @@ -83,7 +83,7 @@ private void RegisterRimTalkHistoryProvider() if (!RimMindPawnLookup.IsActiveSpawnedColonist(pawn)) return null; return BuildRimTalkHistoryContext(pawn!); }, null, ct: ct).ConfigureAwait(false); - }, ModId, stalenessTicks: 3000, invalidationTriggers: new[] { "RimTalkEvent" })); + }, ModId, stalenessTicks: 3000)); // RimTalk history changes are external (no bus event): staleness only } private bool ResolveTypes() diff --git a/Source/Bridge/DialogueGate.cs b/Source/Bridge/DialogueGate.cs index 993a0e7..8277b88 100644 --- a/Source/Bridge/DialogueGate.cs +++ b/Source/Bridge/DialogueGate.cs @@ -1,3 +1,4 @@ +using RimMind.Application.Common.Constants; using RimMind.Bridge.RimTalk.Detection; using RimMind.Bridge.RimTalk.Settings; using Verse; @@ -16,9 +17,9 @@ public static bool ShouldSkipDialogue(Pawn? pawn, string? triggerType) return triggerType switch { - "Chitchat" => settings.skipChitchat, - "Auto" => settings.skipAutoDialogue, - "PlayerInput" => settings.skipPlayerDialogue && !settings.forceRimMindPlayerDialogue, + DialogueTriggerKinds.Chitchat => settings.skipChitchat, + DialogueTriggerKinds.Auto => settings.skipAutoDialogue, + DialogueTriggerKinds.PlayerInput => settings.skipPlayerDialogue && !settings.forceRimMindPlayerDialogue, _ => false }; } diff --git a/Source/Bridge/RimTalkBridgeCoordinator.cs b/Source/Bridge/RimTalkBridgeCoordinator.cs index cceb5cc..eb41f66 100644 --- a/Source/Bridge/RimTalkBridgeCoordinator.cs +++ b/Source/Bridge/RimTalkBridgeCoordinator.cs @@ -1,16 +1,16 @@ using System.Collections.Generic; -using RimMind.Bridge.RimTalk.Detection; using RimMind.Application.Common.Interfaces.Extension; +using RimMind.Application.Features.Bridges; +using RimMind.Bridge.RimTalk.Detection; using Verse; namespace RimMind.Bridge.RimTalk.Bridge { /// - /// Coordinates registration of RimTalk bridge modules. + /// Coordinates registration of RimTalk bridge modules using Core's . /// Modules are iterated uniformly; each module decides whether to actually register /// based on its own settings/detector checks inside . - /// Unregister iterates all modules but each module's Unregister is a no-op if not registered, - /// making Register/Unregister symmetric (previously Register was conditional, Unregister unconditional). + /// Unregister iterates all modules symmetrically. /// public static class RimTalkBridgeCoordinator { @@ -26,12 +26,14 @@ private static List GetModules() => new PersonaPushBridge() }; - private static BridgeModuleCoordinator CreateCoordinator() => - new BridgeModuleCoordinator( - GetModules(), + private static ExternalModBridgeCoordinator CreateCoordinator() => + new ExternalModBridgeCoordinator( () => RimTalkDetector.IsRimTalkActive, - message => Log.Message($"[RimMind-Bridge-RimTalk] {message}"), - message => Log.Warning($"[RimMind-Bridge-RimTalk] {message}")); + GetModules(), + gates: null, + logInfo: message => Log.Message($"[RimMind-Bridge-RimTalk] {message}"), + logWarn: message => Log.Warning($"[RimMind-Bridge-RimTalk] {message}"), + inactiveMessage: "RimTalk not active, bridge modules skipped."); public static void Register() { diff --git a/Source/Detection/RimTalkDetector.cs b/Source/Detection/RimTalkDetector.cs index 7337dc3..34da57c 100644 --- a/Source/Detection/RimTalkDetector.cs +++ b/Source/Detection/RimTalkDetector.cs @@ -1,5 +1,5 @@ -using System; using HarmonyLib; +using RimMind.Application.Features.Bridges; using Verse; namespace RimMind.Bridge.RimTalk.Detection @@ -7,11 +7,17 @@ namespace RimMind.Bridge.RimTalk.Detection public static class RimTalkDetector { public const string RimTalkPackageId = "cj.rimtalk"; - - private static bool? _cachedResult; - private static int _cacheTick = -1; + private const string RimTalkApiTypeName = "RimTalk.API.RimTalkPromptAPI"; private const int CacheIntervalTicks = 6000; + internal static readonly SoftDependencyDetector Detector = new SoftDependencyDetector( + RimTalkPackageId, + new[] { RimTalkApiTypeName }, + ModsConfig.IsActive, + AccessTools.TypeByName, + () => SafeTicksGame, + CacheIntervalTicks); + private static int SafeTicksGame { get @@ -31,52 +37,19 @@ private static int SafeTicksGame public static bool IsRimTalkActive { - get - { - int now = SafeTicksGame; - if (_cachedResult == null || (now > 0 && now - _cacheTick > CacheIntervalTicks)) - { - _cachedResult = ModsConfig.IsActive(RimTalkPackageId); - if (now > 0) - { - _cacheTick = now; - } - } - return _cachedResult.Value; - } - internal set - { - _cachedResult = value; - } + get => Detector.IsModActive(); + internal set => Detector.SetForcedModActiveForTesting(value); } public static void InvalidateCache() { - _cachedResult = null; - _cacheTick = -1; - _apiAvailable = null; - _apiChecked = false; + Detector.ResetCache(); } - private static bool? _apiAvailable; - private static bool _apiChecked; - public static bool IsRimTalkApiAvailable { - get - { - if (!_apiChecked) - { - _apiAvailable = IsRimTalkActive && AccessTools.TypeByName("RimTalk.API.RimTalkPromptAPI") != null; - _apiChecked = true; - } - return _apiAvailable ?? false; - } - internal set - { - _apiAvailable = value; - _apiChecked = true; - } + get => Detector.IsAvailable(); + internal set => Detector.SetForcedApiAvailableForTesting(value); } } } diff --git a/Source/Extensions/RimTalkDialogueSkipCheck.cs b/Source/Extensions/RimTalkDialogueSkipCheck.cs index 995bd52..8cf140e 100644 --- a/Source/Extensions/RimTalkDialogueSkipCheck.cs +++ b/Source/Extensions/RimTalkDialogueSkipCheck.cs @@ -2,11 +2,12 @@ namespace RimMind.Bridge.RimTalk { - internal sealed class RimTalkDialogueSkipCheck : ISkipCheck + internal sealed class RimTalkDialogueSkipCheck : IChannelSkipCheck { public string Id => "rimtalk_bridge_dialogue"; public string OwnerModId => "RimMindBridgeRimTalk"; public SkipCheckKind Kind => SkipCheckKind.Dialogue; - public bool ShouldSkip(in SkipCheckArgs args) => Bridge.DialogueGate.ShouldSkipDialogue(args.Pawn as Verse.Pawn, args.Trigger); + public string Channel => SkipCheckChannels.Dialogue; + public bool ShouldSkip(in SkipCheckArgs args) => Bridge.DialogueGate.ShouldSkipDialogue(args.Pawn as Verse.Pawn, args.Trigger ?? args.Tag); } } diff --git a/Source/Extensions/RimTalkFloatMenuSkipCheck.cs b/Source/Extensions/RimTalkFloatMenuSkipCheck.cs index 29778ea..a64dffb 100644 --- a/Source/Extensions/RimTalkFloatMenuSkipCheck.cs +++ b/Source/Extensions/RimTalkFloatMenuSkipCheck.cs @@ -2,11 +2,12 @@ namespace RimMind.Bridge.RimTalk { - internal sealed class RimTalkFloatMenuSkipCheck : ISkipCheck + internal sealed class RimTalkFloatMenuSkipCheck : IChannelSkipCheck { public string Id => "rimtalk_bridge_floatmenu"; public string OwnerModId => "RimMindBridgeRimTalk"; public SkipCheckKind Kind => SkipCheckKind.FloatMenu; + public string Channel => SkipCheckChannels.FloatMenu; public bool ShouldSkip(in SkipCheckArgs args) => Bridge.DialogueGate.ShouldSkipFloatMenuOption(); } }