Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
62 changes: 19 additions & 43 deletions Source/Bridge/BridgeModuleCoordinator.cs
Original file line number Diff line number Diff line change
@@ -1,64 +1,40 @@
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<IBridgeModule> _modules;
private readonly Func<bool> _isDependencyActive;
private readonly Action<string> _log;
private readonly Action<string> _warn;
private readonly ExternalModBridgeCoordinator _inner;

public BridgeModuleCoordinator(
IReadOnlyList<IBridgeModule> modules,
Func<bool> isDependencyActive,
Action<string> log,
Action<string> 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();
}
}
2 changes: 1 addition & 1 deletion Source/Bridge/ContextPullBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 4 additions & 3 deletions Source/Bridge/DialogueGate.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using RimMind.Application.Common.Constants;
using RimMind.Bridge.RimTalk.Detection;
using RimMind.Bridge.RimTalk.Settings;
using Verse;
Expand All @@ -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
};
}
Expand Down
20 changes: 11 additions & 9 deletions Source/Bridge/RimTalkBridgeCoordinator.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Coordinates registration of RimTalk bridge modules.
/// Coordinates registration of RimTalk bridge modules using Core's <see cref="ExternalModBridgeCoordinator"/>.
/// Modules are iterated uniformly; each module decides whether to actually register
/// based on its own settings/detector checks inside <see cref="IBridgeModule.Register"/>.
/// 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.
/// </summary>
public static class RimTalkBridgeCoordinator
{
Expand All @@ -26,12 +26,14 @@ private static List<IBridgeModule> 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()
{
Expand Down
57 changes: 15 additions & 42 deletions Source/Detection/RimTalkDetector.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
using System;
using HarmonyLib;
using RimMind.Application.Features.Bridges;
using Verse;

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
Expand All @@ -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);
}
}
}
5 changes: 3 additions & 2 deletions Source/Extensions/RimTalkDialogueSkipCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
3 changes: 2 additions & 1 deletion Source/Extensions/RimTalkFloatMenuSkipCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Loading