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
39 changes: 39 additions & 0 deletions Source/Core/DialogueContextChangedEvent.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.Dialogue.Core
{
/// <summary>
/// Published on the Core bus when a pawn's dialogue partner changes or a dialogue reply was
/// applied (thoughts/relation updated). The cached <c>dialogue_state</c> and
/// <c>dialogue_relation</c> context keys name <see cref="EventName"/> as their invalidation
/// trigger, so the prompt never describes the previous recipient or stale dialogue thoughts.
/// </summary>
public sealed class DialogueContextChangedEvent : AgentBusEvent
{
/// <summary>Bus event name used by context-key invalidation triggers.</summary>
public const string EventName = "DialogueEvent";

public DialogueContextChangedEvent(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<DialogueContextChangedEvent>(EventName); }
catch (Exception ex) { RimMindErrors.Warn($"[RimMind-Dialogue] 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 DialogueContextChangedEvent(pawnId)); }
catch (Exception ex) { RimMindErrors.Warn($"[RimMind-Dialogue] Dialogue change notification failed: {ex.Message}"); }
}
}
}
115 changes: 97 additions & 18 deletions Source/Core/DialogueContextProviderRegistrar.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.Common;
using RimMind.Domain.ValueObjects;
using RimMind.Presentation.Api;
Expand All @@ -16,15 +18,7 @@
internal static class DialogueContextProviderRegistrar
{
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);

/// <summary>
/// Context providers run on pipeline thread-pool threads; pawn memories, relations, dialogue
Expand All @@ -35,14 +29,19 @@

internal static void RegisterAll()
{
// 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).
DialogueContextChangedEvent.Register();
RegisterScenarios();

RimMindAPI.Context.ContextKeys.Register(new ContextProviderDef(
"dialogue_state", ContextLayer.L3_State, 0.2f,
(ctx, ct) =>
{
if (ctx.PawnId <= 0) return Task.FromResult<string?>(null);
int pawnId = ctx.PawnId;
return OnMainThread(() => BuildStateContext(pawnId), ct);
}, RimMindOwnerConsts.DialogueModId, stalenessTicks: 750, invalidationTriggers: new[] { "DialogueEvent" }));
}, RimMindOwnerConsts.DialogueModId, stalenessTicks: 750, invalidationTriggers: new[] { DialogueContextChangedEvent.EventName }));

RimMindAPI.Context.ContextKeys.Register(new ContextProviderDef(
"dialogue_relation", ContextLayer.L3_State, 0.15f,
Expand All @@ -51,7 +50,7 @@
if (ctx.PawnId <= 0) return Task.FromResult<string?>(null);
int pawnId = ctx.PawnId;
return OnMainThread(() => BuildRelationContext(pawnId), ct);
}, RimMindOwnerConsts.DialogueModId, stalenessTicks: 750, invalidationTriggers: new[] { "DialogueEvent" }));
}, RimMindOwnerConsts.DialogueModId, stalenessTicks: 750, invalidationTriggers: new[] { DialogueContextChangedEvent.EventName }));

RimMindAPI.Context.ContextKeys.Register(new ContextProviderDef(
"dialogue_task", ContextLayer.L0_Static, 0.95f,
Expand All @@ -60,8 +59,9 @@
if (ctx.PawnId <= 0) return Task.FromResult<string?>(null);
if (ctx.Scenario != RimMindAPI.Context.ScenarioDialogue) return Task.FromResult<string?>(null);
string? speakerName = ctx.Hints?.TryGetValue("SpeakerName", out var sn) == true ? sn?.ToString() : null;
if (!string.IsNullOrEmpty(speakerName)) return Task.FromResult<string?>(null);
bool isMonologue = ctx.Hints?.TryGetValue("IsMonologue", out var im) == true && im is bool b && b;
bool isMonologue = string.IsNullOrEmpty(speakerName)
&& ctx.Hints?.TryGetValue("IsMonologue", out var im) == true
&& ((im is bool b && b) || (im is string imStr && bool.TryParse(imStr, out var parsed) && parsed));
var subKeys = new List<string> { "Role", "Process", "Constraint", "Fallback", "ThoughtRules" };
subKeys.Add(isMonologue ? "GoalMonologue" : "GoalDialogue");
subKeys.Add(isMonologue ? "ExampleMonologue" : "ExampleDialogue");
Expand All @@ -72,7 +72,7 @@
return OnMainThread(
() => RimMindAPI.Prompt.BuildTaskInstruction("RimMind.Dialogue.Prompt.TaskInstruction", null, keys),
ct);
}, RimMindOwnerConsts.DialogueModId, stalenessTicks: 0, invalidationTriggers: new[] { "DialogueEvent" }));
}, RimMindOwnerConsts.DialogueModId, stalenessTicks: 0));

RimMindAPI.Context.ContextKeys.Register(new ContextProviderDef(
"dialogue_history", ContextLayer.L4_History, 0.5f,
Expand All @@ -81,7 +81,86 @@
if (ctx.PawnId <= 0) return Task.FromResult<string?>(null);
int pawnId = ctx.PawnId;
return OnMainThread(() => BuildHistoryContext(pawnId), ct);
}, RimMindOwnerConsts.DialogueModId, stalenessTicks: 0, invalidationTriggers: new[] { "DialogueEvent" }));
}, RimMindOwnerConsts.DialogueModId, stalenessTicks: 0));
}

private static void RegisterScenarios()
{
ScenarioRegistry.Register(new ScenarioDescriptor
{
Id = ScenarioIds.Dialogue,
DefaultBaseRounds = 10,
Description = "RimMind.Application.Scenario.Dialogue",
DefaultBudget = 0.6f,
L4Mode = L4Mode.BudgetControlled,
DefaultExcludeKeys = new[] { "combat_status", "task_progress" },
OwnerModId = RimMindOwnerConsts.DialogueModId,
RelevanceWeights = new Dictionary<string, float>(StringComparer.Ordinal)
{
["health"] = 0.6f,
["mood"] = 0.9f,
["current_job"] = 0.5f,
["combat_status"] = 0.4f,
["target_info"] = 0.3f,
["task_progress"] = 0.3f,
["nearby_pawns"] = 0.8f,
["colony_status"] = 0.5f,
["current_area"] = 0.6f,
["weather"] = 0.3f,
["time_of_day"] = 0.4f,
["season"] = 0.2f,
["map_structure"] = 0.2f,
["pawn_base_info"] = 0.7f,
["fixed_relations"] = 0.85f,
["ideology"] = 0.6f,
["skills_summary"] = 0.4f,
["memory_pawn"] = 0.7f,
["working_memory"] = 0.6f,
["memory_narrator"] = 0.3f,
["dialogue_state"] = 0.9f,
["dialogue_relation"] = 0.95f,
["dialogue_history"] = 0.85f,
["dialogue_task"] = 0.95f
}
});

ScenarioRegistry.Register(new ScenarioDescriptor
{
Id = ScenarioIds.CrisisDialogue,
DefaultBaseRounds = 8,
Description = "RimMind.Application.Scenario.CrisisDialogue",
DefaultBudget = 0.7f,
L4Mode = L4Mode.BudgetControlled,
DefaultExcludeKeys = new[] { "task_progress" },
OwnerModId = RimMindOwnerConsts.DialogueModId,
RelevanceWeights = new Dictionary<string, float>(StringComparer.Ordinal)
{
["health"] = 0.9f,
["mood"] = 1.0f,
["current_job"] = 0.6f,
["combat_status"] = 0.8f,
["target_info"] = 0.4f,
["task_progress"] = 0.2f,
["nearby_pawns"] = 0.85f,
["colony_status"] = 0.6f,
["current_area"] = 0.6f,
["weather"] = 0.4f,
["time_of_day"] = 0.3f,
["season"] = 0.2f,
["map_structure"] = 0.3f,
["pawn_base_info"] = 0.8f,
["fixed_relations"] = 0.9f,
["ideology"] = 0.7f,
["skills_summary"] = 0.4f,
["memory_pawn"] = 0.8f,
["working_memory"] = 0.85f,
["memory_narrator"] = 0.3f,
["dialogue_state"] = 0.95f,
["dialogue_relation"] = 0.85f,
["dialogue_history"] = 0.5f,
["dialogue_task"] = 0.95f
}
});
}

/// <summary>Main thread only.</summary>
Expand All @@ -99,9 +178,9 @@
var t = memories[i];
if (t?.def == null || t.def.defName != "RimMindDialogue_Thought") continue;

string desc = (t as Thought_RimMindDialogue)?.aiDescription ?? t.def.label;

Check warning on line 181 in Source/Core/DialogueContextProviderRegistrar.cs

View workflow job for this annotation

GitHub Actions / test / test

Converting null literal or possible null value to non-nullable type.

Check warning on line 181 in Source/Core/DialogueContextProviderRegistrar.cs

View workflow job for this annotation

GitHub Actions / test / test

Converting null literal or possible null value to non-nullable type.
float hours = (float)t.DurationTicks / RimMindTime.TicksPerHour;
sb.AppendLine(Tr("RimMind.Dialogue.Context.ThoughtRemaining", desc, $"{hours:F1}"));

Check warning on line 183 in Source/Core/DialogueContextProviderRegistrar.cs

View workflow job for this annotation

GitHub Actions / test / test

Possible null reference argument for parameter 'args' in 'string DialogueContextProviderRegistrar.Tr(string key, params object[] args)'.

Check warning on line 183 in Source/Core/DialogueContextProviderRegistrar.cs

View workflow job for this annotation

GitHub Actions / test / test

Possible null reference argument for parameter 'args' in 'string DialogueContextProviderRegistrar.Tr(string key, params object[] args)'.
any = true;
}
return any ? sb.ToString().TrimEnd() : null;
Expand Down Expand Up @@ -138,7 +217,7 @@

var directRel = pawn.relations?.DirectRelations?.FirstOrDefault(r => r != null && r.otherPawn == recipient);
if (directRel?.def != null)
sb.AppendLine(Tr("RimMind.Dialogue.Context.DirectRelation", directRel.def.label));

Check warning on line 220 in Source/Core/DialogueContextProviderRegistrar.cs

View workflow job for this annotation

GitHub Actions / test / test

Possible null reference argument for parameter 'args' in 'string DialogueContextProviderRegistrar.Tr(string key, params object[] args)'.

Check warning on line 220 in Source/Core/DialogueContextProviderRegistrar.cs

View workflow job for this annotation

GitHub Actions / test / test

Possible null reference argument for parameter 'args' in 'string DialogueContextProviderRegistrar.Tr(string key, params object[] args)'.

return sb.ToString().TrimEnd();
}
Expand Down Expand Up @@ -178,9 +257,9 @@
relevant.Reverse();

var sb = new StringBuilder();
sb.AppendLine("RimMind.Dialogue.Context.HistoryHeader".CanTranslate()
? "RimMind.Dialogue.Context.HistoryHeader".Translate().ToString()
: "【Recent Dialogue History】(Strictly forbidden to mechanically repeat the same complaint or phrase. Expand on new work details or mutual care):");
sb.AppendLine(RimMindText.TrOr(
"RimMind.Dialogue.Context.HistoryHeader",
"【Recent Dialogue History】(Strictly forbidden to mechanically repeat the same complaint or phrase. Expand on new work details or mutual care):"));

foreach (var entry in relevant)
{
Expand Down
Loading
Loading