This commit is contained in:
iisaev committed 2026-10-18 12:41:37 +03:00
commit b6dd3758cb
32 files changed
+2999

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
using System.Collections.Generic;
using Cthangover.Core.Characters;
using Cthangover.Core.Mods;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.Utils;
using Godot;
namespace Cthangover.Core.Factories.Impls
{
/// <summary>
/// Factory for battle action cards equipped by characters. Returns a
/// <b>copy</b> of the cached <c>ActionCharacter</c> on every <c>Get</c>
/// call — action cards carry mutable battle state (cooldowns, modifiers)
/// and must never be shared across character instances. Properties are
/// parsed from a compact <c>key=value</c> string list rather than a full
/// JSON object, keeping mod JSON terse for authored content.
///
/// Images are loaded via <c>TextureUtils.LoadFromModGroup</c> — not
/// through a texture factory — because action character assets live
/// inside the same <c>"characters"</c> asset group as their PNGs,
/// making co-location of data and art the default mod layout. The JSON
/// definitions themselves are found by kind, not by that directory.
/// <para/>
/// The factory lives in the characters engine mod; its singleton is also
/// published to the kernel through <see cref="ActionTemplateRegistry"/> by
/// <c>CharactersModInitializer</c>, so core-side consumers resolve
/// templates without referencing this assembly.
/// </summary>
public class ActionCharacterFactory : ICacheLoader<string, ActionCharacter>, IActionTemplateProvider
{
/// <summary>Process-wide singleton for this mod assembly; the public API for action template lookups.</summary>
public static ActionCharacterFactory Instance { get; } = new ActionCharacterFactory();
private Dictionary<string, ActionCharacterInfo> _allInfos;
private readonly BoundedCache<string, ActionCharacter> _cache;
internal ActionCharacterFactory()
{
var size = ModConfig.Instance.Cache.GetCacheSize("actions", 128);
_cache = new BoundedCache<string, ActionCharacter>(size, this);
}
ActionCharacter ICacheLoader<string, ActionCharacter>.Load(string id)
{
var infos = EnsureInfos();
if (infos.TryGetValue(id, out var info))
return CreateActionCharacter(info);
return null;
}
/// <summary>
/// Returns the action definitions, collecting them on first use.
/// Publication to <c>_allInfos</c> waits until the mod registry is
/// finalized so an early lookup from the launcher boot window cannot
/// freeze an empty snapshot for the session.
/// </summary>
private Dictionary<string, ActionCharacterInfo> EnsureInfos()
{
if (_allInfos != null)
return _allInfos;
var infos = Entities.Collect<ActionCharacterInfo>();
if (ModManager.Instance.IsInitialized)
_allInfos = infos;
return infos;
}
/// <summary>
/// Returns an independent copy of the action definition for
/// <paramref name="id"/>, or <c>null</c> when unknown. The copy is
/// deliberate: action cards carry mutable battle state, so callers
/// never share one instance.
/// </summary>
public ActionCharacter Get(string id)
{
EnsureInfos();
var item = _cache.Get(id);
return item?.Copy();
}
/// <summary>
/// Returns all known action info IDs. Used by the character panel to determine
/// which actions are not assigned to any character (the action pool).
/// </summary>
public IReadOnlyCollection<string> GetAllActionIds()
{
return EnsureInfos()?.Keys;
}
private ActionCharacter CreateActionCharacter(ActionCharacterInfo info)
{
if (info == null)
return null;
var item = new ActionCharacter
{
ID = info.ID,
Name = info.Name,
Description = info.Description,
Type = info.Type,
Properties = ParseValues(info.Values),
Image = LoadImage(info.Image),
};
return item;
}
private Texture2D LoadImage(string imagePath)
{
return TextureUtils.LoadFromModGroup("characters", imagePath);
}
private static PropertyData ParseValues(List<string> values)
{
var props = new PropertyData();
if (values == null)
return props;
foreach (var value in values)
{
var idx = value.IndexOf('=');
if (idx > 0)
props.Values[value.Substring(0, idx)] = value.Substring(idx + 1);
}
return props;
}
}
}
+30
View File
@@ -0,0 +1,30 @@
using Cthangover.Core.Factories;
using Cthangover.Core.Utils;
using Godot;
namespace Cthangover.Core.Factories.Impls
{
/// <summary>
/// Thin <c>Texture2DFactory</c> for character portrait avatars. The
/// separate factory (as opposed to reusing <c>UITextureFactory</c>)
/// isolates avatar images into their own mod group with a dedicated
/// cache budget, so frequently-swapped dialog portraits don't evict
/// other UI textures under memory pressure.
/// <para/>
/// The factory lives in the characters engine mod; its singleton is also
/// published to the kernel through <see cref="AvatarProviderRegistry"/> by
/// <c>CharactersModInitializer</c>, so core-side consumers resolve
/// avatars without referencing this assembly.
/// </summary>
public class AvatarFactory : Texture2DFactory, IAvatarProvider
{
internal AvatarFactory() : base("avatars", 64) { }
/// <summary>Process-wide singleton for this mod assembly; the public API for avatar lookups.</summary>
public static AvatarFactory Instance { get; } = new AvatarFactory();
/// <summary>Mod asset group scanned by this factory — files live under <c>mods/*/</c><b>avatars</b>.</summary>
public override string GroupName => "avatars";
}
}
+157
View File
@@ -0,0 +1,157 @@
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.Characters;
using Cthangover.Core.Mods;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.Utils;
using Godot;
namespace Cthangover.Core.Factories.Impls
{
/// <summary>
/// Factory for full character definitions. Returns a <b>copy</b> of
/// the cached <c>Character</c> on every <c>Get</c> call because
/// <c>Character.Attributes</c> mutate during gameplay.
///
/// Character actions are resolved through
/// <c>ActionCharacterFactory</c>. Attribute fields that are missing
/// or zero in JSON receive nonzero fallback values so incomplete
/// mod data doesn't produce unplayable characters.
///
/// Patching is handled transparently by the shared
/// <c>EntityResolver</c> — the factory receives already-merged
/// <c>CharacterInfo</c> DTOs routed by their <c>character</c> kind and
/// simply maps them to the runtime model.
/// <para/>
/// The factory lives in the characters engine mod; its singleton is also
/// published to the kernel through <see cref="CharacterTemplateRegistry"/>
/// by <c>CharactersModInitializer</c>, so core-side consumers resolve
/// templates without referencing this assembly.
/// </summary>
public class CharacterFactory : ICacheLoader<string, Character>, ICharacterTemplateProvider
{
/// <summary>Process-wide singleton for this mod assembly; the public API for template lookups.</summary>
public static CharacterFactory Instance { get; } = new CharacterFactory();
private Dictionary<string, CharacterInfo> _allInfos;
private readonly BoundedCache<string, Character> _cache;
internal CharacterFactory()
{
var size = ModConfig.Instance.Cache.GetCacheSize("characters", 128);
_cache = new BoundedCache<string, Character>(size, this);
}
Character ICacheLoader<string, Character>.Load(string id)
{
var infos = EnsureInfos();
GameLogger.Log("FACTORY", $"CharacterFactory.Load('{id}'): infos count={infos?.Count ?? -1}", LogLevel.Debug);
if (infos.TryGetValue(id, out var info))
return CreateCharacter(info);
return null;
}
/// <summary>
/// Returns the character definitions, collecting them on first use.
/// The result is published to <c>_allInfos</c> only after the mod
/// registry is finalized. A stale empty snapshot cannot appear: the
/// registry refuses mod-data access until <c>Boot.IsReady</c>, so the
/// first collection always sees the final mod set, and the cached
/// snapshot is reset when <c>ModManager.Reload</c> rebuilds the
/// registry.
/// </summary>
private Dictionary<string, CharacterInfo> EnsureInfos()
{
if (_allInfos != null)
return _allInfos;
GameLogger.Log("FACTORY", $"CharacterFactory.EnsureInfos: START, ModRegistry.IsInitialized={ModRegistry.Instance.IsInitialized}, mods count={ModRegistry.Instance.Mods?.Count ?? 0}", LogLevel.Debug);
var infos = Entities.Collect<CharacterInfo>();
if (ModManager.Instance.IsInitialized)
_allInfos = infos;
GameLogger.Log("FACTORY", $"CharacterFactory.EnsureInfos: END, loaded {infos?.Count ?? 0} infos", LogLevel.Debug);
return infos;
}
/// <summary>
/// Returns an independent copy of the character template for
/// <paramref name="id"/>, or <c>null</c> (with an error log) when the id
/// is unknown. The copy semantics matter: battle and party systems mutate
/// attributes freely without touching the cached template.
/// </summary>
public Character Get(string id)
{
var infos = EnsureInfos();
if (!infos.ContainsKey(id))
{
GameLogger.Log("FACTORY", $"CharacterFactory.Get('{id}'): NOT FOUND in infos", LogLevel.Error);
return null;
}
var item = _cache.Get(id);
if(item?.Image == null)
GameLogger.Log("FACTORY", $"CharacterFactory.Get('{id}'): found, image=null", LogLevel.Error);
return item?.Copy();
}
private static Character CreateCharacter(CharacterInfo info)
{
if (info == null)
return null;
var item = new Character
{
ID = info.ID,
Behaviour = info.Behaviour,
Name = info.Name,
Level = info.Level,
Exp = info.Exp,
RecruitmentChance = info.RecruitmentChance,
Actions = ResolveActions(info.Actions ?? new List<string>()),
Image = LoadImage(info.Image),
Loot = info.Loot,
};
item.Attributes.Health.Init(info.Health > 0 ? info.Health : 1);
item.Attributes.Defence.Init(info.Defence);
item.Attributes.Attack.Init(info.Attack);
item.Attributes.Point.Init(info.Point > 0 ? info.Point : 1);
PropertyData.ImportJsonProperties(item.Attributes.Properties, info.ExtraProperties);
return item;
}
private static List<ActionCharacter> ResolveActions(List<string> actions)
{
if (actions == null || actions.Count == 0)
return new List<ActionCharacter>();
return actions
.Select(id => ActionCharacterFactory.Instance.Get(id))
.Where(card => card != null)
.ToList();
}
private static Texture2D LoadImage(string imagePath)
{
if (string.IsNullOrWhiteSpace(imagePath))
{
GameLogger.Log("FACTORY", $"LoadImage: imagePath is null/empty", LogLevel.Error);
return null;
}
var tex = TextureUtils.LoadFromModGroup("characters", imagePath);
if(tex == null)
GameLogger.Log("FACTORY", $"LoadImage: result=null", LogLevel.Error);
return tex;
}
}
}