This commit is contained in:
iisaev committed 2026-10-18 12:41:40 +03:00
commit b1ecbdf51b
22 files changed
+1384

No files matched your search

+164
View File
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.Characters;
using Cthangover.Core.Settings;
using Cthangover.Core.Utils;
namespace Cthangover.Core.Relationship
{
/// <summary>
/// Central registry and lifecycle dispatcher for recruit behaviours
/// and conditions. Behaviours (<see cref="IRecruitBehaviour"/>) and
/// conditions (<see cref="IRecruitCondition"/>) of every mod arrive
/// through the kernel's assembly consumers
/// (<see cref="RecruitBehaviourConsumer"/>, <see cref="RecruitConditionConsumer"/>)
/// as mods load, and leave again when their mod unloads — the kernel
/// stays the only place that enumerates assemblies.
///
/// <b>Condition gating</b> — <see cref="CanRecruit"/> evaluates every
/// registered condition conjunctively (logical AND), so a single
/// <c>false</c> blocks recruitment. This allows mods to compose
/// recruitment gates without coordination.
///
/// <b>Id-based deduplication</b> — a registration replaces any earlier
/// implementation with the same <c>Id</c>, so two mods shipping the same
/// id never double-dispatch notifications.
///
/// <b>Tick registration</b> — the first call to any lifecycle method
/// triggers <see cref="RecruitTickController.EnsureRegistered"/>,
/// which registers for the scene's timer event. This avoids paying
/// the tick overhead unless at least one behaviour is active.
///
/// <b>Singleton</b> — accessed via the static <c>Instance</c> field
/// rather than a <c>Lazy&lt;T&gt;</c> wrapper because Godot
/// reflection-based discovery must tolerate partial-initialisation
/// scenarios where <c>Lazy</c>'s thread-safety check could deadlock.
/// </summary>
public class RecruitBehaviourRegistry
{
/// <summary>Eagerly-initialised singleton for reflection-safe access.</summary>
public static readonly RecruitBehaviourRegistry Instance = new();
private readonly List<IRecruitBehaviour> _behaviours = new();
private readonly List<IRecruitCondition> _conditions = new();
private readonly RecruitTickController _tickController = new();
private void EnsureTickRegistered()
{
_tickController.EnsureRegistered();
}
/// <summary>
/// Registers a behaviour, replacing an earlier one with the same Id. Fed
/// by <see cref="RecruitBehaviourConsumer"/> for every mod assembly.
/// </summary>
public void RegisterBehaviour(IRecruitBehaviour behaviour)
{
if (behaviour == null)
return;
_behaviours.RemoveAll(existing => existing.Id == behaviour.Id);
_behaviours.Add(behaviour);
}
/// <summary>Removes exactly <paramref name="behaviour"/> (its mod is being unloaded).</summary>
public void UnregisterBehaviour(IRecruitBehaviour behaviour)
{
_behaviours.Remove(behaviour);
}
/// <summary>
/// Registers a condition, replacing an earlier one with the same Id. Fed
/// by <see cref="RecruitConditionConsumer"/> for every mod assembly.
/// </summary>
public void RegisterCondition(IRecruitCondition condition)
{
if (condition == null)
return;
_conditions.RemoveAll(existing => existing.Id == condition.Id);
_conditions.Add(condition);
}
/// <summary>Removes exactly <paramref name="condition"/> (its mod is being unloaded).</summary>
public void UnregisterCondition(IRecruitCondition condition)
{
_conditions.Remove(condition);
}
/// <summary>
/// Conjunctive gate: returns <c>true</c> only if every registered
/// <see cref="IRecruitCondition"/> returns <c>true</c> for the
/// given enemy and runtime state.
/// </summary>
public bool CanRecruit(Character enemy, RuntimeData runtime)
{
return _conditions.All(c => c.CanRecruit(enemy, runtime));
}
/// <summary>
/// Looks up a behaviour by its <c>Id</c> (linear scan —
/// behaviour lists are small enough that a dictionary is
/// unnecessary). Returns <c>null</c> if not found.
/// </summary>
public IRecruitBehaviour Get(string id)
{
return _behaviours.Find(b => b.Id == id);
}
/// <summary>Returns all registered behaviours for external enumeration.</summary>
public IEnumerable<IRecruitBehaviour> All()
{
return _behaviours;
}
/// <summary>
/// Dispatches <see cref="IRecruitBehaviour.OnTick"/> to every
/// behaviour for every active recruit. Ensures tick registration
/// on first call via lazy init.
/// </summary>
public void OnTick()
{
EnsureTickRegistered();
var runtime = GameData.Instance.Runtime;
if (runtime == null)
return;
var recruits = RecruitingData.Instance.Data;
foreach (var behaviour in _behaviours)
{
foreach (var recruit in recruits)
{
behaviour.OnTick(recruit, runtime, runtime.Time.Tick);
}
}
}
/// <summary>
/// Dispatches <see cref="IRecruitBehaviour.ConfigureRecruit"/> to
/// every behaviour for the newly-added <paramref name="recruit"/>.
/// </summary>
public void OnConfigure(Recruit recruit)
{
EnsureTickRegistered();
var runtime = GameData.Instance.Runtime;
if (runtime == null)
return;
foreach (var behaviour in _behaviours)
behaviour.ConfigureRecruit(recruit, runtime);
}
/// <summary>
/// Dispatches <see cref="IRecruitBehaviour.OnRemove"/> to every
/// behaviour for the departing <paramref name="recruit"/>.
/// </summary>
public void OnRemove(Recruit recruit)
{
EnsureTickRegistered();
var runtime = GameData.Instance.Runtime;
if (runtime == null)
return;
foreach (var behaviour in _behaviours)
behaviour.OnRemove(recruit, runtime);
}
}
}