Files
bansonic_beta_main/Assets/playerInfoDisplay/uAchievement/globalAchievement/GlobalAchievementService.cs
T

572 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
public sealed class GlobalAchievementService : MonoBehaviour
{
private const string SaveCategory = "global_achievement";
private const string SaveKey = "runtime_state";
public const string DefaultResourcesPath = "so/globalachievments";
public const string DefaultEditorAssetFolder = "Assets/Resources/so/globalachievments";
public static GlobalAchievementService Instance { get; private set; }
public event Action<GlobalAchievementSO> OnAchievementUnlocked;
private readonly List<GlobalAchievementSO> definitions = new List<GlobalAchievementSO>();
private readonly Dictionary<int, GlobalAchievementStateEntry> stateById = new Dictionary<int, GlobalAchievementStateEntry>();
private readonly Dictionary<GlobalAchievementMetricType, GlobalAchievementMetricEntry> metricByType = new Dictionary<GlobalAchievementMetricType, GlobalAchievementMetricEntry>();
private GlobalAchievementSaveData saveData;
private bool initialized;
private bool loginReportedThisSession;
private int lastSaveFrame = -1;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static GlobalAchievementService EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__global_achievement_service");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<GlobalAchievementService>();
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;
GlobalAchievementSaveData loaded;
if (!SecureSaveVault.TryLoadJson(SaveCategory, SaveKey, out loaded))
{
loaded = new GlobalAchievementSaveData();
}
saveData = loaded ?? new GlobalAchievementSaveData();
RebuildCaches();
LoadDefinitionsFromResources();
ReportLoginSessionStart();
RefreshDerivedMetrics();
SaveNow();
}
public void LoadDefinitionsFromResources(string resourcesPath = DefaultResourcesPath)
{
InitializeIfNeeded();
definitions.Clear();
GlobalAchievementSO[] loadedDefinitions = Resources.LoadAll<GlobalAchievementSO>(resourcesPath);
if (loadedDefinitions != null && loadedDefinitions.Length > 0)
{
definitions.AddRange(loadedDefinitions);
definitions.Sort(CompareDefinitions);
}
SyncMirrorFlagsToDefinitions();
EvaluateAll();
}
public IReadOnlyList<GlobalAchievementSO> GetDefinitions()
{
InitializeIfNeeded();
return definitions;
}
public List<GlobalAchievementSnapshot> GetSnapshots()
{
InitializeIfNeeded();
var snapshots = new List<GlobalAchievementSnapshot>(definitions.Count);
for (int i = 0; i < definitions.Count; i++)
{
GlobalAchievementSO definition = definitions[i];
if (definition == null || definition.achievementId <= 0)
{
continue;
}
GlobalAchievementStateEntry state = GetOrCreateState(definition.achievementId);
float currentValue = GetMetric(definition.metricType);
float targetValue = Mathf.Max(1f, definition.targetValue);
snapshots.Add(new GlobalAchievementSnapshot
{
definition = definition,
currentValue = currentValue,
normalizedProgress = Mathf.Clamp01(currentValue / targetValue),
unlocked = state.unlocked,
unlockedUtc = state.unlockedUtcTicks > 0 ? new DateTime(state.unlockedUtcTicks, DateTimeKind.Utc) : default(DateTime),
unlockedDateTimeText = state.unlockedUtcTicks > 0 ? new DateTime(state.unlockedUtcTicks, DateTimeKind.Utc).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") : string.Empty,
});
}
return snapshots;
}
public float GetMetric(GlobalAchievementMetricType metricType)
{
InitializeIfNeeded();
GlobalAchievementMetricEntry metric;
if (metricByType.TryGetValue(metricType, out metric))
{
return Mathf.Max(0f, metric.value);
}
return 0f;
}
public void AddMetric(GlobalAchievementMetricType metricType, float delta)
{
if (Mathf.Approximately(delta, 0f))
{
return;
}
InitializeIfNeeded();
GlobalAchievementMetricEntry metric = GetOrCreateMetric(metricType);
float nextValue = Mathf.Max(0f, metric.value + delta);
if (Mathf.Approximately(metric.value, nextValue))
{
return;
}
metric.value = nextValue;
PersistMetricsIfChanged(true);
}
public void SetMetric(GlobalAchievementMetricType metricType, float value)
{
InitializeIfNeeded();
PersistMetricsIfChanged(SetMetricValue(metricType, value));
}
public void SetMetricAtLeast(GlobalAchievementMetricType metricType, float value)
{
InitializeIfNeeded();
GlobalAchievementMetricEntry metric = GetOrCreateMetric(metricType);
float nextValue = Mathf.Max(0f, value);
if (nextValue <= metric.value)
{
return;
}
metric.value = nextValue;
PersistMetricsIfChanged(true);
}
public void ReportLoginSessionStart()
{
InitializeIfNeeded();
if (loginReportedThisSession)
{
return;
}
loginReportedThisSession = true;
string currentDateStamp = DateTime.Now.ToString("yyyy-MM-dd");
if (!string.IsNullOrEmpty(saveData.trustedDateStamp) && string.CompareOrdinal(currentDateStamp, saveData.trustedDateStamp) < 0)
{
currentDateStamp = saveData.trustedDateStamp;
}
else
{
saveData.trustedDateStamp = currentDateStamp;
}
if (!string.Equals(saveData.lastLoginDateStamp, currentDateStamp, StringComparison.Ordinal))
{
saveData.lastLoginDateStamp = currentDateStamp;
AddMetric(GlobalAchievementMetricType.LoginDays, 1f);
return;
}
SaveNow();
}
public void ReportCurrentCoins(int currentCoins)
{
// No current-coin achievement type now, but this remains available as a central probe point
// for future expansion without changing economy call sites.
}
public void ReportCoinsEarned(int amount)
{
if (amount <= 0)
{
return;
}
AddMetric(GlobalAchievementMetricType.TotalCoinsEarned, amount);
}
public void RefreshOwnedHeroCount()
{
InitializeIfNeeded();
PersistMetricsIfChanged(RefreshOwnedHeroCountInternal(true));
}
public void RefreshOwnedSongCountAndScores()
{
InitializeIfNeeded();
PersistMetricsIfChanged(RefreshOwnedSongCountAndScoresInternal(true));
}
public void RefreshDerivedMetrics()
{
InitializeIfNeeded();
StoreOwnershipLedger.EnsureInstance().ForceSyncMirrorFlags();
bool changed = false;
changed |= RefreshOwnedHeroCountInternal(false);
changed |= RefreshOwnedSongCountAndScoresInternal(false);
PersistMetricsIfChanged(changed);
}
public void SaveNow()
{
if (!initialized || saveData == null)
{
return;
}
if (Application.isPlaying && lastSaveFrame == Time.frameCount)
{
return;
}
lastSaveFrame = Application.isPlaying ? Time.frameCount : -1;
SecureSaveVault.SaveJson(SaveCategory, SaveKey, saveData);
}
private void EvaluateAll()
{
for (int i = 0; i < definitions.Count; i++)
{
GlobalAchievementSO definition = definitions[i];
if (definition == null || definition.achievementId <= 0)
{
continue;
}
GlobalAchievementStateEntry state = GetOrCreateState(definition.achievementId);
if (state.unlocked)
{
continue;
}
float currentValue = GetMetric(definition.metricType);
if (currentValue + 0.0001f < Mathf.Max(0f, definition.targetValue))
{
continue;
}
Unlock(definition, state);
}
SyncMirrorFlagsToDefinitions();
}
private bool RefreshOwnedHeroCountInternal(bool syncOwnershipFirst)
{
if (syncOwnershipFirst)
{
StoreOwnershipLedger.EnsureInstance().ForceSyncMirrorFlags();
}
AllyHero_SO[] heroes = Resources.LoadAll<AllyHero_SO>("so/ally");
if (heroes == null || heroes.Length == 0)
{
return SetMetricValue(GlobalAchievementMetricType.OwnedHeroCount, 0f);
}
var uniqueIds = new HashSet<int>();
int unlockedCount = 0;
for (int i = 0; i < heroes.Length; i++)
{
AllyHero_SO hero = heroes[i];
if (hero == null || hero.ally_heroID <= 0 || !uniqueIds.Add(hero.ally_heroID))
{
continue;
}
if (hero.isUnlocked)
{
unlockedCount++;
}
}
return SetMetricValue(GlobalAchievementMetricType.OwnedHeroCount, unlockedCount);
}
private bool RefreshOwnedSongCountAndScoresInternal(bool syncOwnershipFirst)
{
if (syncOwnershipFirst)
{
StoreOwnershipLedger.EnsureInstance().ForceSyncMirrorFlags();
}
SongData[] songs = RuntimeResourcesCache.LoadSongsFromPath("song_songIndex");
if (songs == null || songs.Length == 0)
{
bool emptyChanged = false;
emptyChanged |= SetMetricValue(GlobalAchievementMetricType.OwnedSongCount, 0f);
emptyChanged |= SetMetricValue(GlobalAchievementMetricType.BestSingleSongTotalScore, 0f);
emptyChanged |= SetMetricValue(GlobalAchievementMetricType.TotalSongBestScoreSum, 0f);
return emptyChanged;
}
var uniqueIds = new HashSet<int>();
int ownedSongCount = 0;
int bestSingleSongScore = 0;
long totalSongBestScore = 0L;
for (int i = 0; i < songs.Length; i++)
{
SongData song = songs[i];
if (song == null || song.songID <= 0 || !uniqueIds.Add(song.songID))
{
continue;
}
song.EnsurePersistentDataLoaded();
if (song.isUnlocked)
{
ownedSongCount++;
}
int highestScore;
int bestDifficulty;
string bestDifficultyName;
song.GetAbsoluteHighestScore(out highestScore, out bestDifficulty, out bestDifficultyName);
if (highestScore > bestSingleSongScore)
{
bestSingleSongScore = highestScore;
}
totalSongBestScore += Mathf.Max(0, highestScore);
}
bool changed = false;
changed |= SetMetricValue(GlobalAchievementMetricType.OwnedSongCount, ownedSongCount);
changed |= SetMetricValue(GlobalAchievementMetricType.BestSingleSongTotalScore, bestSingleSongScore);
changed |= SetMetricValue(GlobalAchievementMetricType.TotalSongBestScoreSum, totalSongBestScore > float.MaxValue ? float.MaxValue : totalSongBestScore);
return changed;
}
private bool SetMetricValue(GlobalAchievementMetricType metricType, float value)
{
GlobalAchievementMetricEntry metric = GetOrCreateMetric(metricType);
float nextValue = Mathf.Max(0f, value);
if (Mathf.Approximately(metric.value, nextValue))
{
return false;
}
metric.value = nextValue;
return true;
}
private void PersistMetricsIfChanged(bool changed)
{
if (!changed)
{
return;
}
EvaluateAll();
SaveNow();
}
private void Unlock(GlobalAchievementSO definition, GlobalAchievementStateEntry state)
{
state.unlocked = true;
state.unlockedUtcTicks = DateTime.UtcNow.Ticks;
definition.SetUnlockedState(true, state.unlockedUtcTicks);
GlobalAchievementSteamBridge.TryUnlock(definition);
if (OnAchievementUnlocked != null)
{
OnAchievementUnlocked(definition);
}
}
private GlobalAchievementMetricEntry GetOrCreateMetric(GlobalAchievementMetricType metricType)
{
GlobalAchievementMetricEntry metric;
if (metricByType.TryGetValue(metricType, out metric))
{
return metric;
}
metric = new GlobalAchievementMetricEntry
{
metricType = metricType,
value = 0f
};
metricByType.Add(metricType, metric);
saveData.metrics.Add(metric);
return metric;
}
private GlobalAchievementStateEntry GetOrCreateState(int achievementId)
{
GlobalAchievementStateEntry state;
if (stateById.TryGetValue(achievementId, out state))
{
return state;
}
state = new GlobalAchievementStateEntry
{
achievementId = achievementId,
unlocked = false,
unlockedUtcTicks = 0L
};
stateById.Add(achievementId, state);
saveData.states.Add(state);
return state;
}
private void RebuildCaches()
{
stateById.Clear();
metricByType.Clear();
if (saveData.metrics == null)
{
saveData.metrics = new List<GlobalAchievementMetricEntry>();
}
if (saveData.states == null)
{
saveData.states = new List<GlobalAchievementStateEntry>();
}
for (int i = 0; i < saveData.metrics.Count; i++)
{
GlobalAchievementMetricEntry entry = saveData.metrics[i];
if (entry == null)
{
continue;
}
metricByType[entry.metricType] = entry;
}
for (int i = 0; i < saveData.states.Count; i++)
{
GlobalAchievementStateEntry entry = saveData.states[i];
if (entry == null || entry.achievementId <= 0)
{
continue;
}
stateById[entry.achievementId] = entry;
}
}
private static int CompareDefinitions(GlobalAchievementSO left, GlobalAchievementSO right)
{
if (left == null && right == null)
{
return 0;
}
if (left == null)
{
return 1;
}
if (right == null)
{
return -1;
}
int seriesCompare = string.CompareOrdinal(left.achievementSeriesKey, right.achievementSeriesKey);
if (seriesCompare != 0)
{
return seriesCompare;
}
int levelCompare = left.achievementLevel.CompareTo(right.achievementLevel);
if (levelCompare != 0)
{
return levelCompare;
}
int sortCompare = left.sortOrder.CompareTo(right.sortOrder);
if (sortCompare != 0)
{
return sortCompare;
}
int metricCompare = left.metricType.CompareTo(right.metricType);
if (metricCompare != 0)
{
return metricCompare;
}
int targetCompare = left.targetValue.CompareTo(right.targetValue);
if (targetCompare != 0)
{
return targetCompare;
}
return left.achievementId.CompareTo(right.achievementId);
}
private void SyncMirrorFlagsToDefinitions()
{
for (int i = 0; i < definitions.Count; i++)
{
GlobalAchievementSO definition = definitions[i];
if (definition == null || definition.achievementId <= 0)
{
continue;
}
GlobalAchievementStateEntry state;
bool unlocked = stateById.TryGetValue(definition.achievementId, out state) && state != null && state.unlocked;
definition.SetUnlockedState(unlocked, unlocked && state != null ? state.unlockedUtcTicks : 0L);
}
}
}