using System.Collections.Generic; using System.Linq; using Cthangover.Core.Items; using Cthangover.Core.Mods; using Cthangover.Core.Mods.Resolvers; using Cthangover.Core.Utils; namespace Cthangover.Core.Factories.Impls { /// /// Factory for crafting recipes. Uses BoundedCache because the /// crafting UI may repeatedly query recipes during scrolling and the /// ingredient resolution (through ItemFactory) is non-trivial. /// Recipes are treated as immutable value objects — Get returns /// the cached instance directly without copying. Missing ingredient /// items emit an error and skip that ingredient rather than failing the /// whole recipe, so a mod author's typo in one input doesn't silently /// break the crafting UI for unrelated recipes. /// /// The single instance lives in this mod assembly as a static lazy /// singleton; the static facade stays as the public API. /// public class RecipeFactory : ICacheLoader { private static RecipeFactory _instance; /// Static lazy singleton; kept as the public API. public static RecipeFactory Instance => _instance ??= new RecipeFactory(); private Dictionary _allInfos; private readonly BoundedCache _cache; internal RecipeFactory() { var size = ModConfig.Instance.Cache.GetCacheSize("recipes", 128); _cache = new BoundedCache(size, this); } IRecipe ICacheLoader.Load(string id) { var infos = EnsureInfos(); if (infos.TryGetValue(id, out var info)) return CreateRecipe(info); return null; } /// /// Returns the recipe definitions, collecting them on first use. /// Publication to _allInfos waits until the mod registry is /// finalized so an early lookup from the launcher boot window cannot /// freeze an empty snapshot for the session. /// private Dictionary EnsureInfos() { // The resolver's dictionary identity is the data snapshot: a new one drops the built recipes. var infos = Entities.Collect(); if (!ReferenceEquals(infos, _allInfos)) { _allInfos = infos; _cache.Clear(); } return infos; } /// /// Returns the cached recipe for (resolved lazily /// through ), or null for unknown /// or empty ids. The shared instance is returned without copying — /// recipes are treated as immutable. /// public IRecipe Get(string id) { if (string.IsNullOrEmpty(id)) return null; EnsureInfos(); return _cache.Get(id); } private static Recipe CreateRecipe(RecipeInfo info) { return new Recipe { ID = info.ID, Name = info.Name, Description = info.Description, Time = info.Time, WorkbenchType = info.Type, Input = info.Input .Select(i => CreateIngredient(i)) .Where(i => i != null) .ToList(), Output = info.Output .Select(i => CreateIngredient(i)) .Where(i => i != null) .ToList() }; } private static IIngredient CreateIngredient(IngredientInfo info) { var item = ItemFactory.Instance.Get(info.ID); if (item == null) { GameLogger.Log("MOD_TEST", $"RecipeFactory: item '{info.ID}' not found in ItemFactory", LogLevel.Error); return null; } return new Ingredient { Item = item, Count = info.Count }; } } }