Skip to content
Open
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
6 changes: 6 additions & 0 deletions HAModHelper.GamePlugin/Base/Base.GamePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using HAModHelper.GamePlugin.Perks.Systems;
using HAModHelper.GamePlugin.Gui.Systems;
using HAModHelper.GamePlugin.Dialogue.Systems;
using HAModHelper.GamePlugin.Creatures.Systems;
using HAModHelper.GamePlugin.Core.Debug;
using System.Net;
using BepInEx.Unity.IL2CPP;
Expand Down Expand Up @@ -94,6 +95,11 @@ public override void Load()
stopwatch6.Stop();
Log.LogInfo($"[HAMH] Initialized DialogueManager in {stopwatch6.ElapsedMilliseconds}ms.");

var stopwatch7 = Stopwatch.StartNew();
CreatureManager.Instance.Initialize();
stopwatch7.Stop();
Log.LogInfo($"[HAMH] Initialized CreatureManager in {stopwatch7.ElapsedMilliseconds}ms.");

//var stopwatch3 = Stopwatch.StartNew();
//UniverseLibConfig uvlconfig = new UniverseLibConfig
//{
Expand Down
1 change: 1 addition & 0 deletions HAModHelper.GamePlugin/Creatures/Creatures.Events.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
namespace HAModHelper.GamePlugin.Creatures.Events;
1 change: 1 addition & 0 deletions HAModHelper.GamePlugin/Creatures/Creatures.Interfaces.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
namespace HAModHelper.GamePlugin.Creatures.Interfaces;
309 changes: 309 additions & 0 deletions HAModHelper.GamePlugin/Creatures/Creatures.Systems.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
using HarmonyLib;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.UI;
using HAModHelper.GamePlugin.Core;

namespace HAModHelper.GamePlugin.Creatures.Systems;

public class Creature
{
public required string Name { get; set; }
public required List<string> DataLines { get; set; }
public byte[]? SpriteBytes { get; set; }
}

public sealed class CreatureManager
{
public static CreatureManager Instance { get; } = new();
private CreatureManager() { }

private readonly Dictionary<string, Creature> _creatures = new();
private readonly Dictionary<string, Sprite> _spriteCache = new();

public void Initialize() { }

public void AddCreature(Creature creature)
{
_creatures[creature.Name] = creature;
}

public void RegisterFromEmbeddedResources(Assembly assembly, string resourcePrefix)
{
var imageBytes = new Dictionary<string, byte[]>();

foreach (string res in assembly.GetManifestResourceNames())
{
if (!res.StartsWith(resourcePrefix) || !res.EndsWith(".png"))
continue;

string name = res.Substring(resourcePrefix.Length, res.Length - resourcePrefix.Length - 4);
try
{
using var stream = assembly.GetManifestResourceStream(res)!;
using var ms = new MemoryStream();
stream.CopyTo(ms);
imageBytes[name] = ms.ToArray();
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] Failed to load creature image {name}: {ex.Message}"); } catch { }
}
}

foreach (string res in assembly.GetManifestResourceNames())
{
if (!res.StartsWith(resourcePrefix) || !res.EndsWith(".txt"))
continue;

string name = res.Substring(resourcePrefix.Length, res.Length - resourcePrefix.Length - 4);
try
{
using var stream = assembly.GetManifestResourceStream(res)!;
using var reader = new StreamReader(stream);
var lines = new List<string>(Regex.Split(reader.ReadToEnd(), @"\r\n|\r|\n"));
AddCreature(new Creature
{
Name = name,
DataLines = lines,
SpriteBytes = imageBytes.TryGetValue(name, out var bytes) ? bytes : null,
});
try { HAMHMod.Logger.LogInfo($"[HAMH] Registered creature: {name}"); } catch { }
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] Failed to load creature {name}: {ex.Message}"); } catch { }
}
}
}

internal bool TryGetDataLines(string name, out List<string> lines)
{
if (_creatures.TryGetValue(name, out var c))
{
lines = c.DataLines;
return true;
}
lines = null!;
return false;
}

internal bool IsCustomCreature(string name) => _creatures.ContainsKey(name);

internal IReadOnlyCollection<string> AllNames => _creatures.Keys;

internal void EnsureInLists()
{
try
{
var morpher = CreatureMorpher.Instance;
if (morpher == null || _creatures.Count == 0) return;

foreach (string name in _creatures.Keys)
{
if (morpher.all_creature_names != null && !morpher.all_creature_names.Contains(name))
morpher.all_creature_names.Add(name);

if (morpher.default_creature_names != null && !morpher.default_creature_names.Contains(name))
morpher.default_creature_names.Add(name);
}
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] EnsureInLists failed: {ex}"); } catch { }
}
}

internal Sprite? GetOrCreateSprite(string name)
{
if (_spriteCache.TryGetValue(name, out var cached))
return cached;

if (!_creatures.TryGetValue(name, out var creature) || creature.SpriteBytes == null)
return null;

var tex = new Texture2D(2, 2);
tex.hideFlags = HideFlags.DontUnloadUnusedAsset;
ImageConversion.LoadImage(tex, creature.SpriteBytes);
var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
sprite.hideFlags = HideFlags.DontUnloadUnusedAsset;
_spriteCache[name] = sprite;
return sprite;
}

private static string ExtractCreatureName(string path)
{
if (string.IsNullOrEmpty(path)) return path;
int slash = path.LastIndexOf('/');
if (slash >= 0) path = path.Substring(slash + 1);
int backslash = path.LastIndexOf('\\');
if (backslash >= 0) path = path.Substring(backslash + 1);
return path;
}

[HarmonyPatch(typeof(ResourceControl), nameof(ResourceControl.GetTextFileLines))]
internal static class GetTextFileLinesPatch
{
static bool Prefix(string __0, ref bool __1, ref Il2CppSystem.Collections.Generic.List<string> __result)
{
try
{
string name = ExtractCreatureName(__0);
if (!Instance.TryGetDataLines(name, out var lines))
return true;

var il2List = new Il2CppSystem.Collections.Generic.List<string>();
foreach (string line in lines)
il2List.Add(line);

__1 = true;
__result = il2List;
return false;
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] GetTextFileLines patch error: {ex}"); } catch { }
return true;
}
}
}

[HarmonyPatch(typeof(CreatureMorpher), nameof(CreatureMorpher.LoadPlainCreatureFromDisk))]
internal static class LoadPlainCreatureFromDiskPatch
{
static void Prefix(string __0)
{
try
{
if (Instance.IsCustomCreature(__0))
Instance.EnsureInLists();
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] LoadPlainCreatureFromDisk patch error: {ex}"); } catch { }
}
}
}

[HarmonyPatch(typeof(CreatureMorpher), nameof(CreatureMorpher.GenerateCreatureLists))]
internal static class GenerateCreatureListsPatch
{
static void Postfix()
{
try
{
Instance.EnsureInLists();
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] GenerateCreatureLists patch error: {ex}"); } catch { }
}
}
}

[HarmonyPatch(typeof(BreedControl), nameof(BreedControl.SetUpBreeder))]
internal static class SetUpBreederPatch
{
// BreedControl.CreateButtons lays buttons out in a 3-column grid with 110 unit spacing.
private const int ButtonColumns = 3;
private const float ButtonSpacing = 110f;

static void Postfix(BreedControl __instance)
{
try
{
if (Instance._creatures.Count == 0) return;
Instance.EnsureInLists();

var customList = new Il2CppSystem.Collections.Generic.List<string>();
foreach (string name in Instance.AllNames)
customList.Add(name);

__instance.CreateButtons(customList, false);

// SetUpBreeder derives the scroll limit from the vanilla button count before this postfix runs,
// so the rows added above would be unreachable. Recompute it with the game's own formula.
__instance.max_buttons_y = ((__instance.n_buttons - 1) / ButtonColumns) * ButtonSpacing;
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] SetUpBreeder patch error: {ex}"); } catch { }
}
}
}

[HarmonyPatch(typeof(BreedControl), nameof(BreedControl.LoadEverythingFromDisk))]
internal static class LoadEverythingFromDiskPatch
{
static void Prefix()
{
try
{
Instance.EnsureInLists();
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] LoadEverythingFromDisk patch error: {ex}"); } catch { }
}
}
}

[HarmonyPatch(typeof(ResourceControl), nameof(ResourceControl.AssignCreatureSprite))]
internal static class AssignCreatureSpritePatch
{
static bool Prefix(string __0, Image __1)
{
try
{
if (!Instance.IsCustomCreature(__0))
return true;

var sprite = Instance.GetOrCreateSprite(__0);
if (sprite != null && __1 != null)
{
__1.sprite = sprite;
__1.enabled = true;
}
return false;
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] AssignCreatureSprite patch error: {ex}"); } catch { }
return false;
}
}
}

[HarmonyPatch(typeof(CreatureMorpher), nameof(CreatureMorpher.GetRandomCreature))]
internal static class GetRandomCreaturePatch
{
static bool Prefix(ref string __result)
{
try
{
if (Instance._creatures.Count == 0) return true;

if (UnityEngine.Random.Range(0f, 1f) < 0.15f)
{
int idx = UnityEngine.Random.Range(0, Instance._creatures.Count);
int i = 0;
foreach (string name in Instance.AllNames)
{
if (i++ == idx)
{
__result = name;
return false;
}
}
}
}
catch (Exception ex)
{
try { HAMHMod.Logger.LogError($"[HAMH] GetRandomCreature patch error: {ex}"); } catch { }
}
return true;
}
}
}
Loading