init mod
This commit is contained in:
commit
b6dd3758cb
32 files changed
+2999
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user