Files
bansonic_beta_main/Assets/scripts/_runtimecache/StoreOwnershipLedger.cs
T
2026-07-16 23:04:59 +08:00

805 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public sealed class StoreOwnershipLedger : MonoBehaviour
{
private const string RuntimeStoreItemResourcesPath = "so/storeSO";
public static StoreOwnershipLedger Instance { get; private set; }
public event Action<int> OnOwnershipChanged;
private readonly Dictionary<int, StoreOwnershipEntry> entriesByItemId = new Dictionary<int, StoreOwnershipEntry>();
private readonly List<storeItemSO> cachedStoreItems = new List<storeItemSO>();
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
private int lastSaveFrame = -1;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static StoreOwnershipLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_ownership_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<StoreOwnershipLedger>();
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;
}
initialized = true;
StoreOwnershipPayload payload;
loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
LoadStoreItems();
bool recoveredFromMirrors = false;
if (!loadedFromSave && !hasAnyRecoverableLocalState)
{
recoveredFromMirrors = SeedFromCurrentMirrorFlags();
}
if (loadedFromSave || recoveredFromMirrors)
{
SyncAllMirrorFlags();
SaveNow();
}
}
public void ForceSyncMirrorFlags()
{
InitializeIfNeeded();
LoadStoreItems();
SyncAllMirrorFlags();
}
public bool IsOwned(storeItemSO itemSO)
{
InitializeIfNeeded();
if (itemSO == null)
{
return false;
}
StoreOwnershipEntry entry;
entriesByItemId.TryGetValue(itemSO.itemID, out entry);
switch (itemSO.itemType)
{
case storeItemSO.ItemType.character:
return (entry != null && entry.owned) || (itemSO.associatedAllyHero != null && itemSO.associatedAllyHero.isUnlocked);
case storeItemSO.ItemType.song:
return (entry != null && entry.owned) || (itemSO.associatedSong != null && itemSO.associatedSong.isUnlocked);
case storeItemSO.ItemType.storyPassage:
return IsStoryGrantOwned(itemSO, entry);
default:
return false;
}
}
public bool TryGrantOwnership(storeItemSO itemSO, out string failureMessage)
{
InitializeIfNeeded();
failureMessage = string.Empty;
if (!ValidateGrantTarget(itemSO, out failureMessage))
{
return false;
}
var entry = GetOrCreateEntry(itemSO.itemID);
switch (itemSO.itemType)
{
case storeItemSO.ItemType.character:
case storeItemSO.ItemType.song:
entry.owned = true;
break;
case storeItemSO.ItemType.storyPassage:
GrantStoryOwnership(itemSO, entry);
break;
default:
failureMessage = LocalizationService.Get("store.error.grant_logic_missing", "当前未配置发放逻辑");
return false;
}
ApplyEntryToItem(itemSO, entry);
GlobalAchievementService.EnsureInstance().RefreshDerivedMetrics();
SaveNow();
NotifyOwnershipChanged(itemSO.itemID);
return true;
}
public bool TryRevokeOwnership(storeItemSO itemSO, out string failureMessage)
{
InitializeIfNeeded();
failureMessage = string.Empty;
if (itemSO == null)
{
failureMessage = LocalizationService.Get("store.error.item_data_missing", "商品数据丢失");
return false;
}
StoreOwnershipEntry entry;
entriesByItemId.TryGetValue(itemSO.itemID, out entry);
if (entry == null && !IsOwned(itemSO))
{
return true;
}
if (entry != null)
{
entry.owned = false;
if (entry.unlockedStorySonIds != null)
{
entry.unlockedStorySonIds.Clear();
}
if (!entry.owned && (entry.unlockedStorySonIds == null || entry.unlockedStorySonIds.Count == 0))
{
entriesByItemId.Remove(itemSO.itemID);
}
}
SyncAllMirrorFlags();
GlobalAchievementService.EnsureInstance().RefreshDerivedMetrics();
SaveNow();
NotifyOwnershipChanged(itemSO.itemID);
return true;
}
public bool TryGrantOwnershipPreview(storeItemSO itemSO, out string failureMessage)
{
InitializeIfNeeded();
return ValidateGrantTarget(itemSO, out failureMessage);
}
public void SaveNow()
{
if (!initialized)
{
return;
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[StoreOwnershipLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older store ownership data.");
return;
}
if (Application.isPlaying && lastSaveFrame == Time.frameCount)
{
return;
}
lastSaveFrame = Application.isPlaying ? Time.frameCount : -1;
StoreOwnershipStorage.TrySave(CreatePayload());
}
public void ClearPersistentState()
{
InitializeIfNeeded();
entriesByItemId.Clear();
SyncAllMirrorFlags();
StoreOwnershipStorage.TrySave(CreatePayload());
}
private void RebuildFromPayload(StoreOwnershipPayload payload)
{
entriesByItemId.Clear();
if (payload == null || payload.entries == null)
{
return;
}
for (int i = 0; i < payload.entries.Count; i++)
{
var entry = payload.entries[i];
if (entry == null)
{
continue;
}
if (entry.unlockedStorySonIds == null)
{
entry.unlockedStorySonIds = new List<int>();
}
entriesByItemId[entry.storeItemId] = entry;
}
}
private StoreOwnershipPayload CreatePayload()
{
var payload = StoreOwnershipStorage.CreateDefaultPayload();
foreach (var pair in entriesByItemId)
{
payload.entries.Add(CloneEntry(pair.Value));
}
return payload;
}
private void LoadStoreItems()
{
cachedStoreItems.Clear();
var loadedItems = RuntimeResourcesCache.LoadAllStoreItems();
var seen = new HashSet<int>();
for (int i = 0; i < loadedItems.Length; i++)
{
var item = loadedItems[i];
if (item == null || !seen.Add(item.itemID))
{
continue;
}
cachedStoreItems.Add(item);
}
}
private bool SeedFromCurrentMirrorFlags()
{
bool changed = false;
for (int i = 0; i < cachedStoreItems.Count; i++)
{
var item = cachedStoreItems[i];
if (item == null || entriesByItemId.ContainsKey(item.itemID))
{
continue;
}
StoreOwnershipEntry seededEntry = TrySeedEntry(item);
if (seededEntry == null)
{
continue;
}
entriesByItemId[item.itemID] = seededEntry;
changed = true;
}
return changed;
}
private StoreOwnershipEntry TrySeedEntry(storeItemSO itemSO)
{
if (itemSO == null)
{
return null;
}
switch (itemSO.itemType)
{
case storeItemSO.ItemType.character:
if (itemSO.associatedAllyHero != null && itemSO.associatedAllyHero.isUnlocked)
{
return new StoreOwnershipEntry { storeItemId = itemSO.itemID, owned = true };
}
break;
case storeItemSO.ItemType.song:
if (itemSO.associatedSong != null && itemSO.associatedSong.isUnlocked)
{
return new StoreOwnershipEntry { storeItemId = itemSO.itemID, owned = true };
}
break;
case storeItemSO.ItemType.storyPassage:
return TrySeedStoryEntry(itemSO);
}
return null;
}
private StoreOwnershipEntry TrySeedStoryEntry(storeItemSO itemSO)
{
var story = itemSO != null ? itemSO.associatedStoryPassage : null;
if (story == null)
{
return null;
}
var entry = new StoreOwnershipEntry { storeItemId = itemSO.itemID };
switch (itemSO.storyPassageGrantMode)
{
case storeItemSO.StoryPassageGrantMode.fatherOnly:
entry.owned = story.isUnlocked;
break;
case storeItemSO.StoryPassageGrantMode.specificSonOnly:
if (HasStorySonUnlocked(story, itemSO.associatedStorySonId))
{
entry.unlockedStorySonIds.Add(itemSO.associatedStorySonId);
}
break;
case storeItemSO.StoryPassageGrantMode.fatherAndSpecificSon:
if (story.isUnlocked || HasStorySonUnlocked(story, itemSO.associatedStorySonId))
{
entry.owned = true;
if (itemSO.associatedStorySonId >= 0)
{
entry.unlockedStorySonIds.Add(itemSO.associatedStorySonId);
}
}
break;
case storeItemSO.StoryPassageGrantMode.allSons:
if (story.isUnlocked || AreAllStorySonsUnlocked(story))
{
entry.owned = true;
AddAllStorySonIds(story, entry.unlockedStorySonIds);
}
break;
}
return entry.owned || entry.unlockedStorySonIds.Count > 0 ? entry : null;
}
private void SyncAllMirrorFlags()
{
ResetAllMirrorFlags();
for (int i = 0; i < cachedStoreItems.Count; i++)
{
var item = cachedStoreItems[i];
if (item == null)
{
continue;
}
StoreOwnershipEntry entry;
if (!entriesByItemId.TryGetValue(item.itemID, out entry))
{
continue;
}
ApplyEntryToItem(item, entry);
}
}
private void ResetAllMirrorFlags()
{
var seenHeroIds = new HashSet<int>();
var seenSongIds = new HashSet<int>();
var seenStoryIds = new HashSet<int>();
for (int i = 0; i < cachedStoreItems.Count; i++)
{
var item = cachedStoreItems[i];
if (item == null)
{
continue;
}
if (item.itemType == storeItemSO.ItemType.character && item.associatedAllyHero != null && seenHeroIds.Add(item.associatedAllyHero.ally_heroID))
{
if (item.associatedAllyHero.isUnlocked)
{
item.associatedAllyHero.isUnlocked = false;
MarkDirty(item.associatedAllyHero);
}
}
else if (item.itemType == storeItemSO.ItemType.song && item.associatedSong != null && seenSongIds.Add(item.associatedSong.songID))
{
if (item.associatedSong.isUnlocked)
{
item.associatedSong.isUnlocked = false;
MarkDirty(item.associatedSong);
}
}
else if (item.itemType == storeItemSO.ItemType.storyPassage && item.associatedStoryPassage != null && seenStoryIds.Add(item.associatedStoryPassage.class_id))
{
var story = item.associatedStoryPassage;
bool changed = false;
if (story.isUnlocked)
{
story.isUnlocked = false;
changed = true;
}
if (story.sonList != null)
{
for (int sonIndex = 0; sonIndex < story.sonList.Count; sonIndex++)
{
var son = story.sonList[sonIndex];
if (son != null && son.son_isUnlocked)
{
son.son_isUnlocked = false;
changed = true;
}
}
}
if (changed)
{
MarkDirty(story);
}
}
}
}
private bool ValidateGrantTarget(storeItemSO itemSO, out string failureMessage)
{
failureMessage = string.Empty;
if (itemSO == null)
{
failureMessage = LocalizationService.Get("store.error.item_data_missing", "商品数据丢失");
return false;
}
if (IsOwned(itemSO))
{
failureMessage = LocalizationService.Get("store.error.item_already_unlocked", "物品已解锁");
return false;
}
switch (itemSO.itemType)
{
case storeItemSO.ItemType.character:
if (itemSO.associatedAllyHero == null)
{
failureMessage = LocalizationService.Get("store.error.character_data_missing", "角色数据未配置");
return false;
}
return true;
case storeItemSO.ItemType.song:
if (itemSO.associatedSong == null)
{
failureMessage = LocalizationService.Get("store.error.song_data_missing", "歌曲数据未配置");
return false;
}
return true;
case storeItemSO.ItemType.storyPassage:
if (itemSO.associatedStoryPassage == null)
{
failureMessage = LocalizationService.Get("store.error.story_data_missing", "剧情数据未配置");
return false;
}
if (RequiresSpecificStorySon(itemSO.storyPassageGrantMode))
{
if (itemSO.associatedStorySonId < 0 || FindStorySonIndex(itemSO.associatedStoryPassage, itemSO.associatedStorySonId) < 0)
{
failureMessage = LocalizationService.Get("store.error.story_chapter_missing", "剧情子章节未配置");
return false;
}
}
return true;
default:
failureMessage = LocalizationService.Get("store.error.grant_logic_missing", "当前未配置发放逻辑");
return false;
}
}
private void GrantStoryOwnership(storeItemSO itemSO, StoreOwnershipEntry entry)
{
if (entry.unlockedStorySonIds == null)
{
entry.unlockedStorySonIds = new List<int>();
}
switch (itemSO.storyPassageGrantMode)
{
case storeItemSO.StoryPassageGrantMode.fatherOnly:
entry.owned = true;
break;
case storeItemSO.StoryPassageGrantMode.specificSonOnly:
AddStorySonId(entry.unlockedStorySonIds, itemSO.associatedStorySonId);
break;
case storeItemSO.StoryPassageGrantMode.fatherAndSpecificSon:
entry.owned = true;
AddStorySonId(entry.unlockedStorySonIds, itemSO.associatedStorySonId);
break;
case storeItemSO.StoryPassageGrantMode.allSons:
entry.owned = true;
AddAllStorySonIds(itemSO.associatedStoryPassage, entry.unlockedStorySonIds);
break;
}
}
private void ApplyEntryToItem(storeItemSO itemSO, StoreOwnershipEntry entry)
{
if (itemSO == null || entry == null)
{
return;
}
switch (itemSO.itemType)
{
case storeItemSO.ItemType.character:
if (entry.owned && itemSO.associatedAllyHero != null)
{
itemSO.associatedAllyHero.isUnlocked = true;
itemSO.associatedAllyHero.SetJoinDateIfMissing();
MarkDirty(itemSO.associatedAllyHero);
}
break;
case storeItemSO.ItemType.song:
if (entry.owned && itemSO.associatedSong != null)
{
itemSO.associatedSong.isUnlocked = true;
MarkDirty(itemSO.associatedSong);
}
break;
case storeItemSO.ItemType.storyPassage:
ApplyStoryEntry(itemSO.associatedStoryPassage, entry);
break;
}
}
private void ApplyStoryEntry(notebook_faterType story, StoreOwnershipEntry entry)
{
if (story == null || entry == null)
{
return;
}
if (entry.owned || (entry.unlockedStorySonIds != null && entry.unlockedStorySonIds.Count > 0))
{
story.isUnlocked = true;
MarkDirty(story);
}
if (entry.unlockedStorySonIds == null || story.sonList == null)
{
return;
}
for (int i = 0; i < entry.unlockedStorySonIds.Count; i++)
{
int targetSonId = entry.unlockedStorySonIds[i];
int sonIndex = FindStorySonIndex(story, targetSonId);
if (sonIndex < 0)
{
continue;
}
story.sonList[sonIndex].son_isUnlocked = true;
MarkDirty(story);
}
}
private bool IsStoryGrantOwned(storeItemSO itemSO, StoreOwnershipEntry entry)
{
if (itemSO == null || itemSO.associatedStoryPassage == null)
{
return false;
}
switch (itemSO.storyPassageGrantMode)
{
case storeItemSO.StoryPassageGrantMode.fatherOnly:
return entry != null && entry.owned;
case storeItemSO.StoryPassageGrantMode.specificSonOnly:
return HasEntryStorySon(entry, itemSO.associatedStorySonId)
|| HasStorySonUnlocked(itemSO.associatedStoryPassage, itemSO.associatedStorySonId);
case storeItemSO.StoryPassageGrantMode.fatherAndSpecificSon:
return (entry != null && entry.owned)
|| HasEntryStorySon(entry, itemSO.associatedStorySonId)
|| HasStorySonUnlocked(itemSO.associatedStoryPassage, itemSO.associatedStorySonId);
case storeItemSO.StoryPassageGrantMode.allSons:
return (entry != null && entry.owned) || AreAllStorySonsUnlocked(itemSO.associatedStoryPassage);
default:
return false;
}
}
private static bool HasEntryStorySon(StoreOwnershipEntry entry, int sonId)
{
if (entry == null || entry.unlockedStorySonIds == null || sonId < 0)
{
return false;
}
for (int i = 0; i < entry.unlockedStorySonIds.Count; i++)
{
if (entry.unlockedStorySonIds[i] == sonId)
{
return true;
}
}
return false;
}
private static bool HasStorySonUnlocked(notebook_faterType story, int sonId)
{
int index = FindStorySonIndex(story, sonId);
return index >= 0 && story.sonList[index].son_isUnlocked;
}
private static bool AreAllStorySonsUnlocked(notebook_faterType story)
{
if (story == null || story.sonList == null || story.sonList.Count == 0)
{
return false;
}
for (int i = 0; i < story.sonList.Count; i++)
{
var son = story.sonList[i];
if (son == null || !son.son_isUnlocked)
{
return false;
}
}
return true;
}
private static int FindStorySonIndex(notebook_faterType story, int sonId)
{
if (story == null || story.sonList == null || sonId < 0)
{
return -1;
}
for (int i = 0; i < story.sonList.Count; i++)
{
var son = story.sonList[i];
if (son != null && son.son_id == sonId)
{
return i;
}
}
return -1;
}
private static bool RequiresSpecificStorySon(storeItemSO.StoryPassageGrantMode mode)
{
return mode == storeItemSO.StoryPassageGrantMode.specificSonOnly
|| mode == storeItemSO.StoryPassageGrantMode.fatherAndSpecificSon;
}
private static void AddStorySonId(List<int> sonIds, int sonId)
{
if (sonIds == null || sonId < 0)
{
return;
}
for (int i = 0; i < sonIds.Count; i++)
{
if (sonIds[i] == sonId)
{
return;
}
}
sonIds.Add(sonId);
}
private static void AddAllStorySonIds(notebook_faterType story, List<int> sonIds)
{
if (story == null || story.sonList == null || sonIds == null)
{
return;
}
for (int i = 0; i < story.sonList.Count; i++)
{
var son = story.sonList[i];
if (son == null)
{
continue;
}
AddStorySonId(sonIds, son.son_id);
}
}
private StoreOwnershipEntry GetOrCreateEntry(int storeItemId)
{
StoreOwnershipEntry entry;
if (entriesByItemId.TryGetValue(storeItemId, out entry))
{
return entry;
}
entry = new StoreOwnershipEntry
{
storeItemId = storeItemId,
owned = false,
unlockedStorySonIds = new List<int>()
};
entriesByItemId[storeItemId] = entry;
return entry;
}
private static StoreOwnershipEntry CloneEntry(StoreOwnershipEntry source)
{
return new StoreOwnershipEntry
{
storeItemId = source.storeItemId,
owned = source.owned,
unlockedStorySonIds = source.unlockedStorySonIds != null
? new List<int>(source.unlockedStorySonIds)
: new List<int>()
};
}
private static void MarkDirty(UnityEngine.Object target)
{
#if UNITY_EDITOR
if (!Application.isPlaying && target != null)
{
EditorUtility.SetDirty(target);
}
#endif
}
private void NotifyOwnershipChanged(int itemId)
{
if (itemId <= 0 || OnOwnershipChanged == null)
{
return;
}
OnOwnershipChanged(itemId);
}
}