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/ActionGate.cs b/Source/Bridge/ActionGate.cs index b4e5796..b49d760 100644 --- a/Source/Bridge/ActionGate.cs +++ b/Source/Bridge/ActionGate.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using RimMind.Bridge.RimChat.Cooldown; using RimMind.Bridge.RimChat.Detection; @@ -8,44 +9,120 @@ namespace RimMind.Bridge.RimChat.Bridge { public static class ActionGate { - private static readonly HashSet DiplomacyActions = new HashSet + private static readonly HashSet DiplomacyActions = new HashSet(StringComparer.OrdinalIgnoreCase) { "adjust_faction", "trigger_incident", + "world.faction", + "diplomacy", + "world.storyteller", + "storyteller.incident", }; - private static readonly HashSet SocialActions = new HashSet + private static readonly HashSet SocialActions = new HashSet(StringComparer.OrdinalIgnoreCase) { "romance_attempt", "romance_breakup", + "pawn.interaction", + "social_relax", + "give_item", + "actions_social_interact", + "social_interact", }; - private static readonly HashSet RecruitActions = new HashSet + private static readonly HashSet RecruitActions = new HashSet(StringComparer.OrdinalIgnoreCase) { "recruit_agree", + "recruit", + "actions_recruit", + "pawn.recruit", + "prisoner.recruit", }; public static bool ShouldSkipAction(string? intentId) { - if (intentId is null || intentId.Length == 0) return false; + if (string.IsNullOrWhiteSpace(intentId)) return false; if (!RimChatDetector.IsRimChatActive) return false; var settings = BridgeRimChatSettings.Get(); if (!settings.enableActionGate) return false; if (settings.forceRimMindActions) return false; - if (settings.skipDiplomacyActions && DiplomacyActions.Contains(intentId)) + if (settings.skipDiplomacyActions && IsDiplomacyAction(intentId)) return true; - if (settings.skipSocialActions && SocialActions.Contains(intentId)) + if (settings.skipSocialActions && IsSocialAction(intentId)) return true; - if (settings.skipRecruitAgree && RecruitActions.Contains(intentId)) + if (settings.skipRecruitAgree && IsRecruitAction(intentId)) return true; return false; } + public static bool IsRomanceAction(string? intentId) + { + if (string.IsNullOrWhiteSpace(intentId)) return false; + string normalized = intentId!.Trim(); + if (normalized.StartsWith("romance_", StringComparison.OrdinalIgnoreCase)) + return true; + if (MatchesPrefix(normalized, "pawn.interaction") + && (normalized.IndexOf("romance", StringComparison.OrdinalIgnoreCase) >= 0 + || normalized.EndsWith(".romance_attempt", StringComparison.OrdinalIgnoreCase) + || normalized.EndsWith(".romance_breakup", StringComparison.OrdinalIgnoreCase))) + { + return true; + } + return normalized.IndexOf("romance", StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static bool IsSocialAction(string? intentId) + { + if (string.IsNullOrWhiteSpace(intentId)) return false; + string normalized = intentId!.Trim(); + if (IsRomanceAction(normalized)) return true; + if (SocialActions.Contains(normalized)) return true; + return MatchesPrefix(normalized, "pawn.interaction") + || normalized.StartsWith("pawn_interaction", StringComparison.OrdinalIgnoreCase) + || normalized.StartsWith("social.", StringComparison.OrdinalIgnoreCase); + } + + public static bool IsDiplomacyAction(string? intentId) + { + if (string.IsNullOrWhiteSpace(intentId)) return false; + string normalized = intentId!.Trim(); + if (DiplomacyActions.Contains(normalized)) return true; + return MatchesPrefix(normalized, "world.faction") + || normalized.StartsWith("faction.", StringComparison.OrdinalIgnoreCase) + || normalized.StartsWith("faction_", StringComparison.OrdinalIgnoreCase) + || MatchesPrefix(normalized, "diplomacy") + || normalized.IndexOf("diplomacy", StringComparison.OrdinalIgnoreCase) >= 0 + || MatchesPrefix(normalized, "world.storyteller") + || MatchesPrefix(normalized, "storyteller.incident") + || normalized.StartsWith("incident.", StringComparison.OrdinalIgnoreCase); + } + + public static bool IsRecruitAction(string? intentId) + { + if (string.IsNullOrWhiteSpace(intentId)) return false; + string normalized = intentId!.Trim(); + if (RecruitActions.Contains(normalized)) return true; + return MatchesPrefix(normalized, "recruit") + || normalized.StartsWith("recruit_", StringComparison.OrdinalIgnoreCase) + || MatchesPrefix(normalized, "pawn.recruit") + || MatchesPrefix(normalized, "prisoner.recruit") + || normalized.IndexOf("recruit", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static bool MatchesPrefix(string value, string prefix) + { + if (string.Equals(value, prefix, StringComparison.OrdinalIgnoreCase)) + return true; + return value.Length > prefix.Length + && value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) + && (value[prefix.Length] == '.' || value[prefix.Length] == ':' || value[prefix.Length] == '_'); + } + public static bool ShouldSkipStorytellerIncident() { if (!RimChatDetector.IsRimChatActive) return false; diff --git a/Source/Bridge/ContextPullBridge.cs b/Source/Bridge/ContextPullBridge.cs index 06c8482..a263ca1 100644 --- a/Source/Bridge/ContextPullBridge.cs +++ b/Source/Bridge/ContextPullBridge.cs @@ -48,7 +48,7 @@ private static void RegisterDiplomacyProvider() () => BuildDiplomacyContext(), null, ct: ct).ConfigureAwait(false); - }, ModId, stalenessTicks: 1500, invalidationTriggers: new[] { "RimChatEvent" })); + }, ModId, stalenessTicks: 1500)); // RimChat state changes are external (no bus event): staleness only } private static string? BuildDiplomacyContext() @@ -146,7 +146,7 @@ private static void RegisterRpgProvider() if (pawn == null) return null; return BuildRpgContext(pawn); }, null, ct: ct).ConfigureAwait(false); - }, ModId, stalenessTicks: 3000, invalidationTriggers: new[] { "RimChatEvent" })); + }, ModId, stalenessTicks: 3000)); // RimChat state changes are external (no bus event): staleness only } internal static Pawn? TryFindPawnById(int pawnId) diff --git a/Source/Bridge/DialogueGate.cs b/Source/Bridge/DialogueGate.cs index 503a8db..3546681 100644 --- a/Source/Bridge/DialogueGate.cs +++ b/Source/Bridge/DialogueGate.cs @@ -1,3 +1,4 @@ +using RimMind.Application.Common.Constants; using RimMind.Bridge.RimChat.Detection; using RimMind.Bridge.RimChat.Settings; using Verse; @@ -14,9 +15,9 @@ public static bool ShouldSkipDialogue(Pawn? pawn, string? triggerType) return triggerType switch { - "Chitchat" => settings.enableChitchatGate, - "Auto" => settings.enableAutoGate, - "PlayerInput" => ShouldSkipPlayerInput(settings), + DialogueTriggerKinds.Chitchat => settings.enableChitchatGate, + DialogueTriggerKinds.Auto => settings.enableAutoGate, + DialogueTriggerKinds.PlayerInput => ShouldSkipPlayerInput(settings), _ => false }; } diff --git a/Source/Bridge/RimChatApiShim.cs b/Source/Bridge/RimChatApiShim.cs index 81cd6c8..6d8c0a6 100644 --- a/Source/Bridge/RimChatApiShim.cs +++ b/Source/Bridge/RimChatApiShim.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Concurrent; using System.Reflection; using HarmonyLib; +using RimMind.Application.Features.Bridges; using RimMind.Domain.ValueObjects; -using Verse; namespace RimMind.Bridge.RimChat.Bridge { @@ -13,40 +12,26 @@ public static class RimChatApiShim private const string DiplomacyManagerTypeName = "RimChat.DiplomacySystem.GameComponent_DiplomacyManager"; private const string RpgArchiveManagerTypeName = "RimChat.Memory.RpgNpcDialogueArchiveManager"; + internal static readonly ReflectionAccessor Accessor = new ReflectionAccessor( + typeName => AccessTools.TypeByName(typeName), + msg => RimMindErrors.Warn(msg), + "[RimMind-Bridge-RimChat]"); + private static Type? _apiType; private static Type? _diplomacyManagerType; private static Type? _rpgArchiveManagerType; private static bool _resolved; - private static readonly ConcurrentDictionary<(Type Type, string Name, BindingFlags Flags), PropertyInfo?> _propertyCache = - new ConcurrentDictionary<(Type Type, string Name, BindingFlags Flags), PropertyInfo?>(); - - private static readonly ConcurrentDictionary<(Type Type, string Name, BindingFlags Flags), FieldInfo?> _fieldCache = - new ConcurrentDictionary<(Type Type, string Name, BindingFlags Flags), FieldInfo?>(); - - private static readonly ConcurrentDictionary _warnedMissingMembers = - new ConcurrentDictionary(StringComparer.Ordinal); - internal static void ConfigureTypesForTesting( Type? apiType, Type? diplomacyManagerType, Type? rpgArchiveManagerType) { + Accessor.ResetForTesting(); _apiType = apiType; _diplomacyManagerType = diplomacyManagerType; _rpgArchiveManagerType = rpgArchiveManagerType; _resolved = true; - _propertyCache.Clear(); - _fieldCache.Clear(); - _warnedMissingMembers.Clear(); - } - - private static void WarnOnce(string key, string message) - { - if (_warnedMissingMembers.TryAdd(key, 1)) - { - RimMindErrors.Warn(message); - } } private static void EnsureResolved() @@ -62,9 +47,9 @@ private static void ResolveTypes() { try { - _apiType = AccessTools.TypeByName(ApiTypeName); - _diplomacyManagerType = AccessTools.TypeByName(DiplomacyManagerTypeName); - _rpgArchiveManagerType = AccessTools.TypeByName(RpgArchiveManagerTypeName); + _apiType = Accessor.GetCachedType(ApiTypeName); + _diplomacyManagerType = Accessor.GetCachedType(DiplomacyManagerTypeName); + _rpgArchiveManagerType = Accessor.GetCachedType(RpgArchiveManagerTypeName); } catch (Exception ex) { @@ -87,101 +72,32 @@ public static Type? RpgArchiveManagerType get { EnsureResolved(); return _rpgArchiveManagerType; } } - public static PropertyInfo? GetCachedProperty(Type type, string propertyName, + public static PropertyInfo? GetCachedProperty( + Type type, + string propertyName, BindingFlags flags = BindingFlags.Public | BindingFlags.Static) - { - if (type == null || string.IsNullOrEmpty(propertyName)) return null; - var prop = _propertyCache.GetOrAdd( - (type, propertyName, flags), - static k => k.Type.GetProperty(k.Name, k.Flags)); - if (prop == null) - { - WarnOnce( - $"prop:{type.FullName}:{propertyName}:{flags}", - $"[RimMind-Bridge-RimChat] Property '{propertyName}' not found on {type.Name}."); - } - return prop; - } + => Accessor.GetCachedProperty(type, propertyName, flags); - public static FieldInfo? GetCachedField(Type type, string fieldName, + public static FieldInfo? GetCachedField( + Type type, + string fieldName, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance) - { - if (type == null || string.IsNullOrEmpty(fieldName)) return null; - var field = _fieldCache.GetOrAdd( - (type, fieldName, flags), - static k => k.Type.GetField(k.Name, k.Flags)); - if (field == null) - { - WarnOnce( - $"field:{type.FullName}:{fieldName}:{flags}", - $"[RimMind-Bridge-RimChat] Field '{fieldName}' not found on {type.Name}."); - } - return field; - } + => Accessor.GetCachedField(type, fieldName, flags); public static object? GetStaticPropertyValue(Type type, string propertyName) - { - if (type == null) - { - RimMindErrors.Warn("[RimMind-Bridge-RimChat] GetStaticPropertyValue: type is null."); - return null; - } + => Accessor.GetStaticPropertyValue(type, propertyName); - try - { - var prop = GetCachedProperty(type, propertyName, BindingFlags.Public | BindingFlags.Static); - if (prop == null) return null; - return prop.GetValue(null); - } - catch (Exception ex) - { - RimMindErrors.Warn($"[RimMind-Bridge-RimChat] GetStaticPropertyValue({type.Name}.{propertyName}) failed: {ex.Message}"); - return null; - } - } - - public static object? GetInstanceFieldValue(object instance, string fieldName, + public static object? GetInstanceFieldValue( + object instance, + string fieldName, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance) - { - if (instance == null) - { - RimMindErrors.Warn("[RimMind-Bridge-RimChat] GetInstanceFieldValue: instance is null."); - return null; - } - - try - { - var field = GetCachedField(instance.GetType(), fieldName, flags); - if (field == null) return null; - return field.GetValue(instance); - } - catch (Exception ex) - { - RimMindErrors.Warn($"[RimMind-Bridge-RimChat] GetInstanceFieldValue({instance.GetType().Name}.{fieldName}) failed: {ex.Message}"); - return null; - } - } + => Accessor.GetInstanceFieldValue(instance, fieldName, flags); /// /// 获取 RimChat manager 的单例实例(反射读取静态 "Instance" 属性)。 /// 统一 BuildDiplomacyContext / BuildRpgContext 的 manager 获取逻辑。 /// public static object? TryGetManagerInstance(Type? managerType, string instancePropertyName = "Instance") - { - if (managerType == null) - { - RimMindErrors.Warn($"[RimMind-Bridge-RimChat] TryGetManagerInstance: managerType is null."); - return null; - } - - var instance = GetStaticPropertyValue(managerType, instancePropertyName); - if (instance == null) - { - RimMindErrors.Warn($"[RimMind-Bridge-RimChat] TryGetManagerInstance: instance is null on {managerType.Name}."); - return null; - } - - return instance; - } + => Accessor.TryGetSingletonInstance(managerType, instancePropertyName); } } diff --git a/Source/Detection/RimChatDetector.cs b/Source/Detection/RimChatDetector.cs index 9ec38ad..a4b9c9f 100644 --- a/Source/Detection/RimChatDetector.cs +++ b/Source/Detection/RimChatDetector.cs @@ -1,4 +1,6 @@ using System; +using HarmonyLib; +using RimMind.Application.Features.Bridges; using Verse; namespace RimMind.Bridge.RimChat.Detection @@ -6,42 +8,33 @@ namespace RimMind.Bridge.RimChat.Detection public static class RimChatDetector { public const string RimChatPackageId = "yancy.rimchat"; - + private const string RimChatApiTypeName = "RimChat.API.RimChatAPI"; private const int CacheIntervalTicks = 6000; - private static Func _isActive = ModsConfig.IsActive; - private static bool? _cachedResult; - private static int _cacheTick = -CacheIntervalTicks; - public static bool IsRimChatActive - { - get - { - int now = SafeTicksGame; - if (_cachedResult == null || now < _cacheTick || now - _cacheTick > CacheIntervalTicks) - { - _cachedResult = _isActive(RimChatPackageId); - _cacheTick = now; - } - return _cachedResult.Value; - } - } + internal static readonly SoftDependencyDetector Detector = new SoftDependencyDetector( + RimChatPackageId, + new[] { RimChatApiTypeName }, + ModsConfig.IsActive, + AccessTools.TypeByName, + () => SafeTicksGame, + CacheIntervalTicks); + + public static bool IsRimChatActive => Detector.IsModActive(); public static void InvalidateCache() { - _cachedResult = null; - _cacheTick = -CacheIntervalTicks; + Detector.ResetCache(); } internal static void UseActiveProbeForTesting(Func probe) { - _isActive = probe ?? throw new ArgumentNullException(nameof(probe)); - InvalidateCache(); + if (probe == null) throw new ArgumentNullException(nameof(probe)); + Detector.ConfigureForTesting(isModActive: probe); } internal static void ResetActiveProbeForTesting() { - _isActive = ModsConfig.IsActive; - InvalidateCache(); + Detector.ResetDelegatesForTesting(); } private static int SafeTicksGame diff --git a/Source/Extensions/RimChatActionSkipCheck.cs b/Source/Extensions/RimChatActionSkipCheck.cs index 7a2958f..241d55e 100644 --- a/Source/Extensions/RimChatActionSkipCheck.cs +++ b/Source/Extensions/RimChatActionSkipCheck.cs @@ -2,11 +2,12 @@ namespace RimMind.Bridge.RimChat { - internal sealed class RimChatActionSkipCheck : ISkipCheck + internal sealed class RimChatActionSkipCheck : IChannelSkipCheck { public string Id => "rimchat_bridge_action"; public string OwnerModId => "RimMindBridgeRimChat"; public SkipCheckKind Kind => SkipCheckKind.Action; - public bool ShouldSkip(in SkipCheckArgs args) => Bridge.ActionGate.ShouldSkipAction(args.IntentId); + public string Channel => SkipCheckChannels.Action; + public bool ShouldSkip(in SkipCheckArgs args) => Bridge.ActionGate.ShouldSkipAction(args.IntentId ?? args.Tag); } } diff --git a/Source/Extensions/RimChatDialogueSkipCheck.cs b/Source/Extensions/RimChatDialogueSkipCheck.cs index 1ee87c5..7dda66b 100644 --- a/Source/Extensions/RimChatDialogueSkipCheck.cs +++ b/Source/Extensions/RimChatDialogueSkipCheck.cs @@ -2,11 +2,12 @@ namespace RimMind.Bridge.RimChat { - internal sealed class RimChatDialogueSkipCheck : ISkipCheck + internal sealed class RimChatDialogueSkipCheck : IChannelSkipCheck { public string Id => "rimchat_bridge_dialogue"; public string OwnerModId => "RimMindBridgeRimChat"; 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/RimChatFloatMenuSkipCheck.cs b/Source/Extensions/RimChatFloatMenuSkipCheck.cs index 3c49eaf..25b99fe 100644 --- a/Source/Extensions/RimChatFloatMenuSkipCheck.cs +++ b/Source/Extensions/RimChatFloatMenuSkipCheck.cs @@ -2,11 +2,12 @@ namespace RimMind.Bridge.RimChat { - internal sealed class RimChatFloatMenuSkipCheck : ISkipCheck + internal sealed class RimChatFloatMenuSkipCheck : IChannelSkipCheck { public string Id => "rimchat_bridge_floatmenu"; public string OwnerModId => "RimMindBridgeRimChat"; public SkipCheckKind Kind => SkipCheckKind.FloatMenu; + public string Channel => SkipCheckChannels.FloatMenu; public bool ShouldSkip(in SkipCheckArgs args) => Bridge.DialogueGate.ShouldSkipFloatMenuOption(); } } diff --git a/Source/Extensions/RimChatStorytellerIncidentSkipCheck.cs b/Source/Extensions/RimChatStorytellerIncidentSkipCheck.cs index b333386..355fc01 100644 --- a/Source/Extensions/RimChatStorytellerIncidentSkipCheck.cs +++ b/Source/Extensions/RimChatStorytellerIncidentSkipCheck.cs @@ -2,11 +2,12 @@ namespace RimMind.Bridge.RimChat { - internal sealed class RimChatStorytellerIncidentSkipCheck : ISkipCheck + internal sealed class RimChatStorytellerIncidentSkipCheck : IChannelSkipCheck { public string Id => "rimchat_bridge_storyteller"; public string OwnerModId => "RimMindBridgeRimChat"; public SkipCheckKind Kind => SkipCheckKind.StorytellerIncident; + public string Channel => SkipCheckChannels.StorytellerIncident; public bool ShouldSkip(in SkipCheckArgs args) => Bridge.ActionGate.ShouldSkipStorytellerIncident(); } } diff --git a/Tests/Contracts/RimChatGateContracts.cs b/Tests/Contracts/RimChatGateContracts.cs index 4e6282d..771191c 100644 --- a/Tests/Contracts/RimChatGateContracts.cs +++ b/Tests/Contracts/RimChatGateContracts.cs @@ -1,3 +1,5 @@ +using RimMind.Application.Common.Constants; +using RimMind.Application.Common.Interfaces.Extension; using RimMind.Bridge.RimChat.Bridge; using RimMind.Bridge.RimChat.Cooldown; using RimMind.Bridge.RimChat.Detection; @@ -49,6 +51,19 @@ public void Dialogue_gate_preserves_trigger_and_float_menu_parity() Assert.False(DialogueGate.ShouldSkipDialogue(null, "PlayerInput")); Assert.False(DialogueGate.ShouldSkipFloatMenuOption()); + }), + ("shared DialogueTriggerKinds constants match DialogueGate switch arms", () => + { + var settings = Reset(active: true); + settings.enableChitchatGate = true; + settings.enableAutoGate = true; + settings.enablePlayerInputGate = true; + settings.skipPlayerDialogue = true; + settings.forceRimMindPlayerDialogue = false; + + Assert.True(DialogueGate.ShouldSkipDialogue(null, DialogueTriggerKinds.Chitchat)); + Assert.True(DialogueGate.ShouldSkipDialogue(null, DialogueTriggerKinds.Auto)); + Assert.True(DialogueGate.ShouldSkipDialogue(null, DialogueTriggerKinds.PlayerInput)); })); } @@ -103,6 +118,48 @@ public void Action_gate_preserves_classification_and_force_boundaries() Assert.False(ActionGate.ShouldSkipAction("adjust_faction")); Assert.False(ActionGate.ShouldSkipAction("romance_attempt")); Assert.False(ActionGate.ShouldSkipAction("recruit_agree")); + }), + ("real mechanism and tool prefixes match romance social recruit and diplomacy gates", () => + { + var settings = Reset(active: true); + settings.enableActionGate = true; + + settings.skipSocialActions = true; + settings.skipDiplomacyActions = false; + settings.skipRecruitAgree = false; + Assert.True(ActionGate.IsRomanceAction("romance_attempt")); + Assert.True(ActionGate.IsRomanceAction("pawn.interaction.romance_attempt")); + Assert.True(ActionGate.IsRomanceAction("pawn.interaction.romance_breakup")); + Assert.True(ActionGate.IsRomanceAction("custom_romance_tag")); + Assert.True(ActionGate.ShouldSkipAction("pawn.interaction")); + Assert.True(ActionGate.ShouldSkipAction("pawn.interaction.social_relax")); + Assert.True(ActionGate.ShouldSkipAction("pawn.interaction.give_item")); + Assert.True(ActionGate.ShouldSkipAction("pawn.interaction.romance_attempt")); + Assert.False(ActionGate.ShouldSkipAction("world.faction")); + + settings.skipSocialActions = false; + settings.skipDiplomacyActions = true; + Assert.True(ActionGate.ShouldSkipAction("world.faction")); + Assert.True(ActionGate.ShouldSkipAction("world.faction.adjust_goodwill")); + Assert.True(ActionGate.ShouldSkipAction("faction.goodwill")); + Assert.True(ActionGate.ShouldSkipAction("diplomacy")); + Assert.True(ActionGate.ShouldSkipAction("storyteller.incident")); + Assert.True(ActionGate.ShouldSkipAction("incident.raid")); + Assert.False(ActionGate.ShouldSkipAction("pawn.interaction")); + + settings.skipDiplomacyActions = false; + settings.skipRecruitAgree = true; + Assert.True(ActionGate.ShouldSkipAction("recruit")); + Assert.True(ActionGate.ShouldSkipAction("pawn.recruit")); + Assert.True(ActionGate.ShouldSkipAction("prisoner.recruit")); + Assert.True(ActionGate.ShouldSkipAction("prisoner.recruit.chat")); + }), + ("skip check extensions expose open string channels", () => + { + Assert.Equal(SkipCheckChannels.Action, new RimChatActionSkipCheck().Channel); + Assert.Equal(SkipCheckChannels.Dialogue, new RimChatDialogueSkipCheck().Channel); + Assert.Equal(SkipCheckChannels.FloatMenu, new RimChatFloatMenuSkipCheck().Channel); + Assert.Equal(SkipCheckChannels.StorytellerIncident, new RimChatStorytellerIncidentSkipCheck().Channel); })); } diff --git a/Tests/RimChatStubs.cs b/Tests/RimChatStubs.cs index fb2d51f..0877828 100644 --- a/Tests/RimChatStubs.cs +++ b/Tests/RimChatStubs.cs @@ -152,13 +152,30 @@ public interface IToggleBehavior : IExtension { bool IsActive { get; } void Togg public interface IDialogueTrigger : IExtension { void Trigger(Verse.Pawn pawn, string context, Verse.Pawn? recipient); } public interface IModCooldown : IExtension { int CooldownTicks { get; } } public enum SkipCheckKind { Dialogue, FloatMenu, Action, StorytellerIncident } + public static class SkipCheckChannels + { + public const string Dialogue = "dialogue"; + public const string FloatMenu = "float_menu"; + public const string Action = "action"; + public const string StorytellerIncident = "storyteller_incident"; + } public readonly struct SkipCheckArgs { + private readonly string? _tag; public object? Pawn { get; init; } + public object? RecipientPawn { get; init; } + public int PawnId { get; init; } + public int RecipientPawnId { get; init; } public string? Trigger { get; init; } public string? IntentId { get; init; } + public string? Tag + { + get => _tag ?? Trigger ?? IntentId; + init => _tag = value; + } } public interface ISkipCheck : IExtension { SkipCheckKind Kind { get; } bool ShouldSkip(in SkipCheckArgs args); } + public interface IChannelSkipCheck : ISkipCheck { string Channel { get; } } public interface IIncidentExecutedListener : IExtension { void OnIncidentExecuted(); } } diff --git a/Tests/RimMindBridgeRimChat.Tests.csproj b/Tests/RimMindBridgeRimChat.Tests.csproj index 516f7df..c7ab3a0 100644 --- a/Tests/RimMindBridgeRimChat.Tests.csproj +++ b/Tests/RimMindBridgeRimChat.Tests.csproj @@ -21,6 +21,12 @@ + + +