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
11 changes: 2 additions & 9 deletions Source/Core/TimeFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using RimMind.Domain.Common;
using RimMind.Presentation.Api;
using Verse;

namespace RimMind.Memory.Core
Expand All @@ -10,15 +11,7 @@ public static class TimeFormatter
private const int TicksPerDay = RimMindTime.TicksPerDay;

private static string Tr(string key, params object[] args)
{
try
{
if (string.IsNullOrEmpty(key)) return string.Empty;
string template = key.CanTranslate() ? key.TranslateSimple() : key;
return (args == null || args.Length == 0) ? template : string.Format(template, args);
}
catch { return key ?? string.Empty; }
}
=> RimMindText.TrOr(key, key ?? string.Empty, args);

public static string FormatTimeAgo(int eventTick, int nowTick)
{
Expand Down
27 changes: 27 additions & 0 deletions Source/DarkMemory/DarkMemoryResultParserPure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using RimMind.Application.Features.Json;

namespace RimMind.Memory.DarkMemory
{
Expand Down Expand Up @@ -57,6 +58,32 @@ public class DarkMemoryResultDto
private static string? TryRepairTruncatedJson(string input)
{
if (string.IsNullOrWhiteSpace(input)) return null;
if (HasUnclosedString(input)) return null;
return JsonRepairer.TryRepairTruncatedJson(input) ?? TryRepairFallback(input);
}

private static bool HasUnclosedString(string input)
{
bool inString = false;
bool escaped = false;
foreach (char c in input)
{
if (inString)
{
if (escaped) escaped = false;
else if (c == '\\') escaped = true;
else if (c == '"') inString = false;
}
else if (c == '"')
{
inString = true;
}
}
return inString;
}

private static string? TryRepairFallback(string input)
{
try
{
JToken.Parse(input);
Expand Down
4 changes: 4 additions & 0 deletions Source/Data/RimMindMemoryWorldComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public void ClearPawnStore(Pawn pawn)
{
_pawnStores.Remove(pawn.thingIDNumber);
}
RimMind.Memory.Injection.MemoryChangedEvent.Publish(pawn.thingIDNumber);
}

private void SaveAllEntriesToStorage()
Expand Down Expand Up @@ -127,17 +128,20 @@ private void MergeFromSnapshot(string? json)
settings.narratorMaxActive,
settings.narratorMaxArchive));
}
RimMind.Memory.Injection.MemoryChangedEvent.Publish(0);
}

public void AddPawnMemory(Pawn pawn, MemoryEntry e, int maxActive, int maxArchive)
{
var store = GetOrCreatePawnStore(pawn);
store.AddActive(e, maxActive, maxArchive);
RimMind.Memory.Injection.MemoryChangedEvent.Publish(pawn.thingIDNumber);
}

public void AddNarratorMemory(MemoryEntry e, int maxActive, int maxArchive)
{
_narratorStore.AddActive(e, maxActive, maxArchive);
RimMind.Memory.Injection.MemoryChangedEvent.Publish(0);
if (RimMindAPI.RemoteSync.IsConfigured)
{
int tick = Find.TickManager.TicksGame;
Expand Down
39 changes: 39 additions & 0 deletions Source/Injection/MemoryChangedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using RimMind.Domain.Events;
using RimMind.Domain.ValueObjects;
using RimMind.Presentation.Api;

namespace RimMind.Memory.Injection
{
/// <summary>
/// Published on the Core bus whenever pawn or narrator memories are added, merged or cleared.
/// The <c>memory_pawn</c> and <c>memory_narrator</c> context keys name <see cref="EventName"/>
/// as their invalidation trigger, so new memories reach the next prompt immediately.
/// <see cref="AgentBusEvent.PawnId"/> is 0 for narrator memories.
/// </summary>
public sealed class MemoryChangedEvent : AgentBusEvent
{
/// <summary>Bus event name used by context-key invalidation triggers.</summary>
public const string EventName = "MemoryEvent";

public MemoryChangedEvent(int pawnId)
{
PawnId = pawnId;
NpcId = pawnId > 0 ? "NPC-" + pawnId : string.Empty;
}

/// <summary>Registers <see cref="EventName"/> with the Core bus. Call before registering context keys.</summary>
internal static void Register()
{
try { RimMindAPI.Bus.RegisterEventType<MemoryChangedEvent>(EventName); }
catch (Exception ex) { RimMindErrors.Warn($"[RimMind-Memory] Event type registration failed: {ex.Message}"); }
}

/// <summary>Publishes from any thread; never throws.</summary>
internal static void Publish(int pawnId)
{
try { RimMindAPI.Bus.PublishOnMainThread(new MemoryChangedEvent(pawnId)); }
catch (Exception ex) { RimMindErrors.Warn($"[RimMind-Memory] Memory change notification failed: {ex.Message}"); }
}
}
}
55 changes: 35 additions & 20 deletions Source/Injection/MemoryContextProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using System.Threading.Tasks;
using RimMind.Application.Common.Constants;
using RimMind.Application.Common.Interfaces.Context;
using RimMind.Application.Common.Models.Context;
using RimMind.Application.Features.Context;
using RimMind.Domain.ValueObjects;
using RimMind.Presentation.Api;
using RimMind.Memory.Core;
Expand All @@ -20,29 +22,42 @@ public static class MemoryContextProvider
private const int PublicProviderPriority = 100;

private static string Tr(string key, params object[] args)
{
try
{
if (string.IsNullOrEmpty(key)) return string.Empty;
string template = key.CanTranslate() ? key.TranslateSimple() : key;
return (args == null || args.Length == 0) ? template : string.Format(template, args);
}
catch { return key ?? string.Empty; }
}
=> RimMindText.TrOr(key, key ?? string.Empty, args);

private static string TrOrFallback(string key, string fallback)
{
try
{
if (!string.IsNullOrEmpty(key) && key.CanTranslate())
return key.TranslateSimple();
}
catch { }
return fallback;
}
=> RimMindText.TrOr(key, fallback);

public static void Register()
{
// Register the typed event before any key names it, so the trigger resolves
// to a real event (unknown names never fall back to the base bus event).
MemoryChangedEvent.Register();

ScenarioRegistry.Register(new ScenarioDescriptor
{
Id = ScenarioIds.Memory,
DefaultBaseRounds = 0,
Description = "RimMind.Application.Scenario.Memory",
DefaultBudget = 0.4f,
L4Mode = L4Mode.None,
DefaultExcludeKeys = new[]
{
"combat_status", "current_job", "mood",
"task_progress", "npc_commands", "target_info"
},
OwnerModId = RimMindOwnerConsts.MemoryModId,
RelevanceWeights = new Dictionary<string, float>(StringComparer.Ordinal)
{
["memory_pawn"] = 1.0f,
["memory_narrator"] = 1.0f,
["working_memory"] = 0.9f,
["colony_status"] = 0.6f,
["nearby_pawns"] = 0.6f,
["pawn_base_info"] = 0.7f,
["fixed_relations"] = 0.7f
}
});

RimMindAPI.Context.ContextKeys.Register(new ContextProviderDef(
"memory_pawn", ContextLayer.L3_State, 0.25f,
async (ctx, ct) =>
Expand Down Expand Up @@ -122,7 +137,7 @@ public static void Register()
return null;
}
}, null, ct: ct).ConfigureAwait(false);
}, RimMindOwnerConsts.MemoryModId, stalenessTicks: 1500, invalidationTriggers: new[] { "MemoryEvent" }));
}, RimMindOwnerConsts.MemoryModId, stalenessTicks: 1500, invalidationTriggers: new[] { MemoryChangedEvent.EventName }));

RimMindAPI.Context.ContextKeys.Register(new ContextProviderDef(
"memory_narrator", ContextLayer.L4_History, 0.6f,
Expand Down Expand Up @@ -192,7 +207,7 @@ public static void Register()
return null;
}
}, null, ct: ct).ConfigureAwait(false);
}, RimMindOwnerConsts.MemoryModId, stalenessTicks: 3000, invalidationTriggers: new[] { "MemoryEvent" }));
}, RimMindOwnerConsts.MemoryModId, stalenessTicks: 3000, invalidationTriggers: new[] { MemoryChangedEvent.EventName }));

RegisterPublicProviders();
}
Expand Down
12 changes: 2 additions & 10 deletions Source/Injection/WorkingMemoryProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,7 @@ namespace RimMind.Memory.Injection
public static class WorkingMemoryProvider
{
private static string Tr(string key, params object[] args)
{
try
{
if (string.IsNullOrEmpty(key)) return string.Empty;
string template = key.CanTranslate() ? key.TranslateSimple() : key;
return (args == null || args.Length == 0) ? template : string.Format(template, args);
}
catch { return key ?? string.Empty; }
}
=> RimMindText.TrOr(key, key ?? string.Empty, args);

// Compatibility reader for saved entries, not an active memory-production path.
public static void Register()
Expand Down Expand Up @@ -61,7 +53,7 @@ public static void Register()
var result = sb.ToString().TrimEnd();
return string.IsNullOrEmpty(result) ? null : result;
}, null, ct: ct).ConfigureAwait(false);
}, RimMindOwnerConsts.MemoryModId, stalenessTicks: 750, invalidationTriggers: new[] { "MemoryEvent" }));
}, RimMindOwnerConsts.MemoryModId, stalenessTicks: 750)); // legacy save data, never produced at runtime: staleness only
}
}
}
61 changes: 27 additions & 34 deletions Source/Triggers/Patch_AddHediff.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using RimMind.Application.Common.Helpers;
using RimMind.Application.Features.GameEvents;
using RimMind.Domain.ValueObjects;
using RimMind.Memory.Data;
using RimMind.Presentation.Api;
Expand All @@ -17,8 +18,7 @@ public static class Patch_AddHediff
public const int InjuryCoalesceWindowTicks = 2500;

private static readonly FieldInfo? PawnField = AccessTools.Field(typeof(Pawn_HealthTracker), "pawn");
private static readonly object CooldownLock = new object();
private static readonly Dictionary<int, int> LastInjuryTickByPawn = new Dictionary<int, int>();
private static readonly KeyedCooldownGate<int> LastInjuryTickByPawn = new KeyedCooldownGate<int>(256);

public static bool ShouldRecordHediff(
bool isBad,
Expand All @@ -30,8 +30,16 @@ public static bool ShouldRecordHediff(
int? lastRecordedTick,
int coalesceWindowTicks = InjuryCoalesceWindowTicks)
{
if (!isBad && !isTendable && !makesSickThought) return false;
if (!isLifeThreatening && severity < MinHediffSeverity) return false;
if (!GameEventSignificancePolicy.IsSignificantInjury(
isBad,
isTendable,
makesSickThought,
isLifeThreatening,
severity,
MinHediffSeverity))
{
return false;
}
if (!isLifeThreatening
&& lastRecordedTick.HasValue
&& nowTick >= lastRecordedTick.Value
Expand Down Expand Up @@ -67,35 +75,20 @@ static void Postfix(Pawn_HealthTracker __instance, Hediff hediff, DamageInfo? di
try
{
int now = Find.TickManager?.TicksGame ?? 0;
lock (CooldownLock)
{
int? lastTick = LastInjuryTickByPawn.TryGetValue(pawn.thingIDNumber, out int recorded) ? recorded : (int?)null;
if (!ShouldRecordHediff(
hediff.def.isBad,
hediff.def.tendable,
hediff.def.makesSickThought,
hediff.Severity,
isLifeThreatening,
now,
lastTick))
{
return;
}

if (LastInjuryTickByPawn.Count > 128)
{
var stale = new List<int>();
foreach (var kv in LastInjuryTickByPawn)
{
if (now < kv.Value || (now - kv.Value) >= InjuryCoalesceWindowTicks * 4)
stale.Add(kv.Key);
}
for (int i = 0; i < stale.Count; i++)
LastInjuryTickByPawn.Remove(stale[i]);
}

LastInjuryTickByPawn[pawn.thingIDNumber] = now;
}
int cooldownTicks = isLifeThreatening ? 0 : InjuryCoalesceWindowTicks;
if (!LastInjuryTickByPawn.TryEnter(pawn.thingIDNumber, now, cooldownTicks))
return;

RimMindAPI.GameEvents.Publish(new HediffAddedGameEvent(
pawn.thingIDNumber,
hediff.def.defName ?? string.Empty,
hediff.Label ?? hediff.def.label ?? hediff.def.defName ?? string.Empty,
hediff.Severity,
hediff.def.isBad,
hediff.def.tendable,
hediff.def.makesSickThought,
isLifeThreatening,
hediff.Part?.Label));

float importance = EstimateImportance(hediff);
string content = BuildContent(pawn, hediff, dinfo);
Expand Down
Loading
Loading