using System; using System.Collections.Generic; using UnityEngine; public sealed class EquipmentConsumableLedger : MonoBehaviour { public static EquipmentConsumableLedger Instance { get; private set; } public event Action OnCountChanged; private readonly Dictionary countsByKey = new Dictionary(StringComparer.Ordinal); private bool initialized; private bool loadedFromSave; private bool hasAnyRecoverableLocalState; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Bootstrap() { EnsureInstance(); } public static EquipmentConsumableLedger EnsureInstance() { if (Instance != null) { return Instance; } var host = new GameObject("__runtime_equipment_consumable_bridge"); host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector; DontDestroyOnLoad(host); Instance = host.AddComponent(); return Instance; } private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); InitializeIfNeeded(); } private void OnApplicationPause(bool pauseStatus) { if (pauseStatus) { SaveNow(); } } private void OnApplicationQuit() { SaveNow(); } public void InitializeIfNeeded() { if (initialized) { return; } EquipmentConsumableLedgerPayload payload; loadedFromSave = EquipmentConsumableLedgerStorage.TryLoad(out payload); hasAnyRecoverableLocalState = loadedFromSave || EquipmentConsumableLedgerStorage.HasAnyRecoverableState(); RebuildFromPayload(payload); initialized = true; SyncMirrorCounts(); // If an older save exists but cannot be read right now, do not overwrite it // with an empty default payload during startup. if (loadedFromSave || !hasAnyRecoverableLocalState) { SaveNow(); } } public int GetCount(EquipmentConsumableKind kind) { return GetCountByKey(EquipmentConsumableCatalog.GetKey(kind)); } public void Add(EquipmentConsumableKind kind, int amount) { if (amount <= 0) { return; } ChangeCount(EquipmentConsumableCatalog.GetKey(kind), amount); } public bool TryConsume(EquipmentConsumableKind kind, int amount) { if (amount <= 0) { return false; } string key = EquipmentConsumableCatalog.GetKey(kind); int current = GetCountByKey(key); if (current < amount) { return false; } ChangeCount(key, -amount); DailyTaskEventHub.ReportUseItem(amount); return true; } public void ResetAllToZero() { InitializeIfNeeded(); for (int i = 0; i < EquipmentConsumableCatalog.All.Count; i++) { countsByKey[EquipmentConsumableCatalog.All[i].Key] = 0; } SaveNow(); NotifyAllCountsChanged(); } public void SaveNow() { if (!initialized) { InitializeIfNeeded(); } if (!loadedFromSave && hasAnyRecoverableLocalState) { Debug.LogWarning("[EquipmentConsumableLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data."); return; } SyncMirrorCounts(); EquipmentConsumableLedgerStorage.TrySave(BuildPayload()); } private void SyncMirrorCounts() { equipmentConsumableSO[] definitions = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < definitions.Length; i++) { equipmentConsumableSO definition = definitions[i]; if (definition == null) { continue; } definition.runtimeOwnedCount = GetCount(definition.consumableKind); } } private int GetCountByKey(string key) { InitializeIfNeeded(); if (!countsByKey.TryGetValue(key, out int count)) { return 0; } return count; } private void ChangeCount(string key, int delta) { InitializeIfNeeded(); int current = GetCountByKey(key); long next = (long)current + delta; if (next < 0) { next = 0; } else if (next > int.MaxValue) { next = int.MaxValue; } SetCountByKey(key, (int)next); } private void SetCountByKey(string key, int value) { int safeValue = Mathf.Max(0, value); int current = GetCountByKey(key); if (current == safeValue) { return; } countsByKey[key] = safeValue; SaveNow(); if (EquipmentConsumableCatalog.TryGetKind(key, out EquipmentConsumableKind kind) && OnCountChanged != null) { OnCountChanged(kind, safeValue); } } private void RebuildFromPayload(EquipmentConsumableLedgerPayload payload) { countsByKey.Clear(); for (int i = 0; i < EquipmentConsumableCatalog.All.Count; i++) { countsByKey[EquipmentConsumableCatalog.All[i].Key] = 0; } if (payload == null || payload.entries == null) { return; } for (int i = 0; i < payload.entries.Count; i++) { var entry = payload.entries[i]; if (entry == null || string.IsNullOrEmpty(entry.key)) { continue; } countsByKey[entry.key] = Mathf.Max(0, entry.count); } } private EquipmentConsumableLedgerPayload BuildPayload() { var payload = EquipmentConsumableLedgerStorage.CreateDefaultPayload(); payload.entries.Clear(); for (int i = 0; i < EquipmentConsumableCatalog.All.Count; i++) { var descriptor = EquipmentConsumableCatalog.All[i]; payload.entries.Add(new EquipmentConsumableEntry { key = descriptor.Key, count = GetCountByKey(descriptor.Key) }); } return payload; } private void NotifyAllCountsChanged() { if (OnCountChanged == null) { return; } for (int i = 0; i < EquipmentConsumableCatalog.All.Count; i++) { var descriptor = EquipmentConsumableCatalog.All[i]; OnCountChanged(descriptor.Kind, GetCountByKey(descriptor.Key)); } } }