using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; public sealed class DailyTaskService : MonoBehaviour { public static DailyTaskService Instance { get; private set; } public event Action OnTasksChanged; private enum ProgressCombineMode { Sum, Max } private const float OnlineDurationFlushStepSeconds = 5f; private const string GameplaySceneName = "gameplay_gameplay"; private const string MainUiSceneName = "UI_UI"; private readonly Queue pendingEvents = new Queue(); private readonly Dictionary definitionById = new Dictionary(StringComparer.Ordinal); private DailyTaskSaveData saveData; private userTasksPool configuredTaskPool; private int configuredTaskMaxSize = 5; private int configuredRefreshMaxTimes = 3; private bool initialized; private bool appFocused = true; private float pendingOnlineDurationSeconds; private bool isGameplayDurationTracking; private float gameplayDurationRealtimeStart; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Bootstrap() { EnsureInstance(); } public static DailyTaskService EnsureInstance() { if (Instance != null) { return Instance; } var host = new GameObject("__daily_task_service"); 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); SceneManager.sceneLoaded -= OnSceneLoaded; SceneManager.sceneLoaded += OnSceneLoaded; InitializeStorageIfNeeded(); } private void Update() { if (!Application.isPlaying || !appFocused) { return; } pendingOnlineDurationSeconds += Time.unscaledDeltaTime; if (pendingOnlineDurationSeconds >= OnlineDurationFlushStepSeconds) { float flushSeconds = Mathf.Floor(pendingOnlineDurationSeconds); pendingOnlineDurationSeconds = Mathf.Max(0f, pendingOnlineDurationSeconds - flushSeconds); ReportEvent(new DailyTaskEventData { taskType = userTasksPool.TaskType.OnlineDuration, amount = flushSeconds }); } } private void OnApplicationFocus(bool hasFocus) { appFocused = hasFocus; if (!hasFocus) { FlushPendingOnlineDuration(); FlushGameplayDuration(); } } private void OnApplicationPause(bool pauseStatus) { if (pauseStatus) { FlushPendingOnlineDuration(); FlushGameplayDuration(); } } private void OnApplicationQuit() { FlushPendingOnlineDuration(); FlushGameplayDuration(); } private void OnDestroy() { if (Instance == this) { FlushPendingOnlineDuration(); FlushGameplayDuration(); SceneManager.sceneLoaded -= OnSceneLoaded; } } public void Configure(userTasksPool taskPool, int taskMaxSize, int refreshMaxTimes) { configuredTaskPool = taskPool; configuredTaskMaxSize = Mathf.Max(1, taskMaxSize); configuredRefreshMaxTimes = Mathf.Max(0, refreshMaxTimes); RebuildDefinitionIndex(); EnsureTodayTasks(); FlushPendingEvents(); NotifyTasksChanged(); } public IReadOnlyList GetActiveTaskViews() { EnsureTodayTasks(); var result = new List(); if (saveData == null || saveData.activeTasks == null) { return result; } for (int i = 0; i < saveData.activeTasks.Count; i++) { var runtimeEntry = saveData.activeTasks[i]; if (runtimeEntry == null || string.IsNullOrEmpty(runtimeEntry.taskID)) { continue; } userTasksPool.TaskDefinition definition; if (!definitionById.TryGetValue(runtimeEntry.taskID, out definition)) { continue; } result.Add(new DailyTaskViewData { definition = definition, runtimeEntry = runtimeEntry }); } return result; } public void ReportEvent(DailyTaskEventData eventData) { InitializeStorageIfNeeded(); if (!IsConfigured()) { pendingEvents.Enqueue(eventData); return; } EnsureTodayTasks(); bool changed = ApplyEventToTrackedProgress(eventData); if (saveData != null && saveData.activeTasks != null) { for (int i = 0; i < saveData.activeTasks.Count; i++) { var runtimeEntry = saveData.activeTasks[i]; if (runtimeEntry == null || runtimeEntry.isClaimed) { continue; } userTasksPool.TaskDefinition definition; if (!definitionById.TryGetValue(runtimeEntry.taskID, out definition)) { continue; } if (!IsEventMatch(definition, eventData)) { continue; } float nextProgress = CalculateProgressForDefinition(definition); bool nextCompleted = nextProgress >= Mathf.Max(0.0001f, definition.targetValue); if (!Mathf.Approximately(runtimeEntry.progress, nextProgress) || runtimeEntry.isCompleted != nextCompleted) { runtimeEntry.progress = nextProgress; runtimeEntry.isCompleted = nextCompleted; changed = true; } } } if (changed) { Save(); NotifyTasksChanged(); } } public bool TryRefreshTasks() { if (!IsConfigured()) { return false; } EnsureTodayTasks(); if (saveData.refreshUsedCount >= configuredRefreshMaxTimes) { return false; } saveData.refreshUsedCount += 1; StampRefreshUsage(); GenerateDailyTasks(); Save(); NotifyTasksChanged(); return true; } public int GetRemainingRefreshCount() { EnsureTodayTasks(); return Mathf.Max(0, configuredRefreshMaxTimes - (saveData != null ? saveData.refreshUsedCount : 0)); } public bool TryClaimReward(string taskID, Player_SO playerData, out string failureMessage) { failureMessage = string.Empty; if (string.IsNullOrWhiteSpace(taskID)) { failureMessage = LocalizationService.Get("daily.error.invalid_task_argument", "任务参数无效"); return false; } EnsureTodayTasks(); DailyTaskRuntimeEntry runtimeEntry; userTasksPool.TaskDefinition definition; if (!TryGetTaskPair(taskID, out runtimeEntry, out definition)) { failureMessage = LocalizationService.Get("daily.error.task_not_found", "未找到任务"); return false; } if (!runtimeEntry.isCompleted) { failureMessage = LocalizationService.Get("daily.error.task_not_completed", "任务尚未完成"); return false; } if (runtimeEntry.isClaimed) { failureMessage = LocalizationService.Get("daily.error.reward_already_claimed", "奖励已领取"); return false; } if (!TryGrantReward(definition, playerData, out failureMessage)) { return false; } runtimeEntry.isClaimed = true; Save(); NotifyTasksChanged(); return true; } public int ClaimAllAvailableRewards(Player_SO playerData, out string failureMessage) { failureMessage = string.Empty; EnsureTodayTasks(); int claimedCount = 0; var taskIds = new List(); var views = GetActiveTaskViews(); for (int i = 0; i < views.Count; i++) { var runtimeEntry = views[i].runtimeEntry; if (runtimeEntry == null || !runtimeEntry.isCompleted || runtimeEntry.isClaimed) { continue; } taskIds.Add(runtimeEntry.taskID); } for (int i = 0; i < taskIds.Count; i++) { string claimError; if (!TryClaimReward(taskIds[i], playerData, out claimError)) { if (claimedCount <= 0) { failureMessage = claimError; } continue; } claimedCount += 1; } if (claimedCount <= 0 && string.IsNullOrEmpty(failureMessage)) { failureMessage = LocalizationService.Get("daily.no_claimable_reward", "暂无可领取奖励"); } return claimedCount; } public void ClearPersistentState() { InitializeStorageIfNeeded(); pendingEvents.Clear(); pendingOnlineDurationSeconds = 0f; isGameplayDurationTracking = false; gameplayDurationRealtimeStart = 0f; saveData = new DailyTaskSaveData(); EnsureRuntimeCollections(); DailyTaskSaveService.ClearPersistentState(); NotifyTasksChanged(); } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { if (string.Equals(scene.name, MainUiSceneName, StringComparison.OrdinalIgnoreCase)) { TryReportDailyLogin(); } if (string.Equals(scene.name, GameplaySceneName, StringComparison.OrdinalIgnoreCase)) { BeginGameplayDurationTracking(); } else { FlushGameplayDuration(); } } private void InitializeStorageIfNeeded() { if (initialized) { return; } saveData = DailyTaskSaveService.Load() ?? new DailyTaskSaveData(); EnsureRuntimeCollections(); initialized = true; } private void EnsureTodayTasks() { InitializeStorageIfNeeded(); if (!IsConfigured()) { return; } bool timeStateChanged; int effectiveTodayStamp = GetEffectiveTodayStamp(out timeStateChanged); string todayKey = FormatDateKey(effectiveTodayStamp); if (saveData == null) { saveData = new DailyTaskSaveData(); } EnsureRuntimeCollections(); bool needRegenerate = saveData.dateStamp != effectiveTodayStamp || !string.Equals(saveData.dateKey, todayKey, StringComparison.Ordinal) || saveData.activeTasks == null || saveData.activeTasks.Count == 0; if (!needRegenerate) { EnsureAccumulatedProgressCompatibility(); if (timeStateChanged) { Save(); } return; } saveData.dateKey = todayKey; saveData.dateStamp = effectiveTodayStamp; saveData.refreshUsedCount = 0; saveData.loginReportedToday = false; saveData.accumulatedProgress = new List(); saveData.uniqueIntProgress = new List(); GenerateDailyTasks(); Save(); } private void GenerateDailyTasks() { if (saveData == null) { saveData = new DailyTaskSaveData(); } saveData.activeTasks = new List(); var candidates = configuredTaskPool.tasks .Where(t => t != null && t.addToTaskPool && !string.IsNullOrWhiteSpace(t.taskID) && t.selectionWeight > 0) .ToList(); if (candidates.Count == 0) { return; } int pickCount = Mathf.Min(configuredTaskMaxSize, candidates.Count); int dateSeed = saveData.dateStamp > 0 ? saveData.dateStamp : GetLocalDateStamp(DateTime.Now); var random = new System.Random(unchecked(dateSeed + saveData.refreshUsedCount * 397)); var selectedDefinitions = new List(); while (selectedDefinitions.Count < pickCount && candidates.Count > 0) { int totalWeight = candidates.Sum(t => Mathf.Max(1, t.selectionWeight)); int roll = random.Next(0, totalWeight); int cumulative = 0; int selectedIndex = 0; for (int i = 0; i < candidates.Count; i++) { cumulative += Mathf.Max(1, candidates[i].selectionWeight); if (roll < cumulative) { selectedIndex = i; break; } } selectedDefinitions.Add(candidates[selectedIndex]); candidates.RemoveAt(selectedIndex); } for (int i = 0; i < selectedDefinitions.Count; i++) { var definition = selectedDefinitions[i]; float currentProgress = CalculateProgressForDefinition(definition); saveData.activeTasks.Add(new DailyTaskRuntimeEntry { taskID = definition.taskID, progress = currentProgress, isCompleted = currentProgress >= Mathf.Max(0.0001f, definition.targetValue), isClaimed = false }); } } private bool IsConfigured() { return configuredTaskPool != null && definitionById.Count > 0; } private void RebuildDefinitionIndex() { definitionById.Clear(); if (configuredTaskPool == null || configuredTaskPool.tasks == null) { return; } for (int i = 0; i < configuredTaskPool.tasks.Count; i++) { var definition = configuredTaskPool.tasks[i]; if (definition == null || string.IsNullOrWhiteSpace(definition.taskID)) { continue; } if (!definitionById.ContainsKey(definition.taskID)) { definitionById.Add(definition.taskID, definition); } } } private void EnsureAccumulatedProgressCompatibility() { EnsureRuntimeCollections(); if (saveData.accumulatedProgress.Count > 0 || saveData.activeTasks == null || saveData.activeTasks.Count == 0) { return; } bool changed = false; for (int i = 0; i < saveData.activeTasks.Count; i++) { var runtimeEntry = saveData.activeTasks[i]; if (runtimeEntry == null || string.IsNullOrWhiteSpace(runtimeEntry.taskID)) { continue; } userTasksPool.TaskDefinition definition; if (!definitionById.TryGetValue(runtimeEntry.taskID, out definition)) { continue; } if (definition.taskType == userTasksPool.TaskType.DifferentSongs) { continue; } changed |= AddOrCombineProgress(definition.taskType, Mathf.Max(0f, runtimeEntry.progress), GetCombineMode(definition.taskType)); } if (changed) { Save(); } } private void FlushPendingEvents() { if (!IsConfigured()) { return; } while (pendingEvents.Count > 0) { ReportEvent(pendingEvents.Dequeue()); } } private void TryReportDailyLogin() { EnsureTodayTasks(); if (saveData == null || saveData.loginReportedToday) { return; } saveData.loginReportedToday = true; ReportEvent(new DailyTaskEventData { taskType = userTasksPool.TaskType.Login, amount = 1f }); } private void BeginGameplayDurationTracking() { if (isGameplayDurationTracking) { return; } isGameplayDurationTracking = true; gameplayDurationRealtimeStart = Time.realtimeSinceStartup; } private void FlushGameplayDuration() { if (!isGameplayDurationTracking) { return; } float now = Time.realtimeSinceStartup; float elapsed = Mathf.Max(0f, now - gameplayDurationRealtimeStart); isGameplayDurationTracking = false; gameplayDurationRealtimeStart = 0f; if (elapsed <= 0f) { return; } ReportEvent(new DailyTaskEventData { taskType = userTasksPool.TaskType.GameDuration, amount = elapsed }); } private bool ApplyEventToTrackedProgress(DailyTaskEventData eventData) { switch (eventData.taskType) { case userTasksPool.TaskType.Login: case userTasksPool.TaskType.PlaySong: case userTasksPool.TaskType.FullCombo: case userTasksPool.TaskType.WatchStory: case userTasksPool.TaskType.UseItem: case userTasksPool.TaskType.ShareGame: { bool changed = AddOrCombineProgress(eventData.taskType, Mathf.Max(1f, eventData.amount), ProgressCombineMode.Sum); if (eventData.taskType == userTasksPool.TaskType.PlaySong && eventData.songID > 0) { changed |= AddUniqueIntValue(userTasksPool.TaskType.DifferentSongs, eventData.songID); } return changed; } case userTasksPool.TaskType.TotalScore: case userTasksPool.TaskType.SpendCoins: case userTasksPool.TaskType.GameDuration: case userTasksPool.TaskType.OnlineDuration: return AddOrCombineProgress(eventData.taskType, Mathf.Max(0f, eventData.amount), ProgressCombineMode.Sum); case userTasksPool.TaskType.SingleRunCombo: case userTasksPool.TaskType.SingleRunScore: return AddOrCombineProgress(eventData.taskType, Mathf.Max(0f, eventData.amount), ProgressCombineMode.Max); default: return false; } } private static bool IsEventMatch(userTasksPool.TaskDefinition definition, DailyTaskEventData eventData) { if (definition == null) { return false; } if (definition.taskType == eventData.taskType) { return true; } return definition.taskType == userTasksPool.TaskType.DifferentSongs && eventData.taskType == userTasksPool.TaskType.PlaySong; } private float CalculateProgressForDefinition(userTasksPool.TaskDefinition definition) { if (definition == null) { return 0f; } float targetValue = Mathf.Max(0.0001f, definition.targetValue); switch (definition.taskType) { case userTasksPool.TaskType.DifferentSongs: return Mathf.Min(GetUniqueIntCount(userTasksPool.TaskType.DifferentSongs), targetValue); default: return Mathf.Min(Mathf.Max(0f, GetAccumulatedProgress(definition.taskType)), targetValue); } } private bool AddOrCombineProgress(userTasksPool.TaskType taskType, float amount, ProgressCombineMode combineMode) { EnsureRuntimeCollections(); int taskTypeValue = (int)taskType; for (int i = 0; i < saveData.accumulatedProgress.Count; i++) { var entry = saveData.accumulatedProgress[i]; if (entry == null || entry.taskType != taskTypeValue) { continue; } float nextValue = combineMode == ProgressCombineMode.Max ? Mathf.Max(entry.progress, amount) : Mathf.Max(0f, entry.progress + amount); if (Mathf.Approximately(entry.progress, nextValue)) { return false; } entry.progress = nextValue; return true; } saveData.accumulatedProgress.Add(new DailyTaskAccumulatedProgress { taskType = taskTypeValue, progress = Mathf.Max(0f, amount) }); return true; } private float GetAccumulatedProgress(userTasksPool.TaskType taskType) { if (saveData == null || saveData.accumulatedProgress == null) { return 0f; } int taskTypeValue = (int)taskType; for (int i = 0; i < saveData.accumulatedProgress.Count; i++) { var entry = saveData.accumulatedProgress[i]; if (entry == null || entry.taskType != taskTypeValue) { continue; } return Mathf.Max(0f, entry.progress); } return 0f; } private bool AddUniqueIntValue(userTasksPool.TaskType taskType, int value) { if (value <= 0) { return false; } EnsureRuntimeCollections(); int taskTypeValue = (int)taskType; for (int i = 0; i < saveData.uniqueIntProgress.Count; i++) { var entry = saveData.uniqueIntProgress[i]; if (entry == null || entry.taskType != taskTypeValue) { continue; } if (entry.values == null) { entry.values = new List(); } if (entry.values.Contains(value)) { return false; } entry.values.Add(value); return true; } saveData.uniqueIntProgress.Add(new DailyTaskUniqueIntProgress { taskType = taskTypeValue, values = new List { value } }); return true; } private int GetUniqueIntCount(userTasksPool.TaskType taskType) { if (saveData == null || saveData.uniqueIntProgress == null) { return 0; } int taskTypeValue = (int)taskType; for (int i = 0; i < saveData.uniqueIntProgress.Count; i++) { var entry = saveData.uniqueIntProgress[i]; if (entry == null || entry.taskType != taskTypeValue || entry.values == null) { continue; } return entry.values.Count; } return 0; } private static ProgressCombineMode GetCombineMode(userTasksPool.TaskType taskType) { switch (taskType) { case userTasksPool.TaskType.SingleRunCombo: case userTasksPool.TaskType.SingleRunScore: return ProgressCombineMode.Max; default: return ProgressCombineMode.Sum; } } private void EnsureRuntimeCollections() { if (saveData == null) { saveData = new DailyTaskSaveData(); } if (saveData.activeTasks == null) { saveData.activeTasks = new List(); } if (saveData.accumulatedProgress == null) { saveData.accumulatedProgress = new List(); } if (saveData.uniqueIntProgress == null) { saveData.uniqueIntProgress = new List(); } } private int GetEffectiveTodayStamp(out bool stateChanged) { stateChanged = false; if (saveData == null) { saveData = new DailyTaskSaveData(); } int currentStamp = GetLocalDateStamp(DateTime.Now); long nowLocalTicks = DateTime.Now.Ticks; long nowUtcTicks = DateTime.UtcNow.Ticks; if (saveData.trustedDateStamp <= 0) { saveData.trustedDateStamp = currentStamp; stateChanged = true; } else if (currentStamp < saveData.trustedDateStamp) { saveData.timeRollbackDetections += 1; currentStamp = saveData.trustedDateStamp; stateChanged = true; } else if (currentStamp > saveData.trustedDateStamp) { saveData.trustedDateStamp = currentStamp; stateChanged = true; } if (saveData.lastSeenLocalTicks != nowLocalTicks) { saveData.lastSeenLocalTicks = nowLocalTicks; stateChanged = true; } if (saveData.lastSeenUtcTicks != nowUtcTicks) { saveData.lastSeenUtcTicks = nowUtcTicks; stateChanged = true; } return Mathf.Max(currentStamp, saveData.trustedDateStamp); } private void StampRefreshUsage() { saveData.lastRefreshDateStamp = saveData.dateStamp; saveData.lastRefreshLocalTicks = DateTime.Now.Ticks; saveData.lastRefreshUtcTicks = DateTime.UtcNow.Ticks; } private bool TryGetTaskPair(string taskID, out DailyTaskRuntimeEntry runtimeEntry, out userTasksPool.TaskDefinition definition) { runtimeEntry = null; definition = null; if (saveData == null || saveData.activeTasks == null) { return false; } for (int i = 0; i < saveData.activeTasks.Count; i++) { var candidate = saveData.activeTasks[i]; if (candidate == null || !string.Equals(candidate.taskID, taskID, StringComparison.Ordinal)) { continue; } userTasksPool.TaskDefinition matchedDefinition; if (!definitionById.TryGetValue(candidate.taskID, out matchedDefinition)) { return false; } runtimeEntry = candidate; definition = matchedDefinition; return true; } return false; } private static bool TryGrantReward(userTasksPool.TaskDefinition definition, Player_SO playerData, out string failureMessage) { failureMessage = string.Empty; if (definition == null) { failureMessage = LocalizationService.Get("daily.error.invalid_definition", "任务定义无效"); return false; } if (definition.rewardAmount <= 0) { failureMessage = LocalizationService.Get("daily.error.invalid_reward_amount", "奖励数量无效"); return false; } switch (definition.rewardType) { case userTasksPool.RewardType.coins: { var wallet = PlayerEconomyLedger.EnsureInstance(); if (playerData != null) { wallet.AttachPlayerData(playerData); } wallet.AddCoins(definition.rewardAmount); return true; } case userTasksPool.RewardType.expBottle: { var bottleLedger = ExpBottleLedger.EnsureInstance(); if (playerData != null) { bottleLedger.AttachPlayerData(playerData); } bottleLedger.Add(definition.rewardExpBottleKind, definition.rewardAmount); return true; } case userTasksPool.RewardType.dushMaterial: { var materialLedger = DushMaterialLedger.EnsureInstance(); if (playerData != null) { materialLedger.AttachPlayerData(playerData); } materialLedger.Add(definition.rewardDushMaterialKind, definition.rewardAmount); return true; } default: failureMessage = LocalizationService.Get("daily.error.unsupported_reward_type", "暂不支持该奖励类型"); return false; } } private void FlushPendingOnlineDuration() { if (pendingOnlineDurationSeconds <= 0f) { return; } float flushSeconds = pendingOnlineDurationSeconds; pendingOnlineDurationSeconds = 0f; ReportEvent(new DailyTaskEventData { taskType = userTasksPool.TaskType.OnlineDuration, amount = flushSeconds }); } private static int GetLocalDateStamp(DateTime dateTime) { return dateTime.Year * 10000 + dateTime.Month * 100 + dateTime.Day; } private static string FormatDateKey(int dateStamp) { int year = dateStamp / 10000; int month = (dateStamp / 100) % 100; int day = dateStamp % 100; return year.ToString("D4") + "-" + month.ToString("D2") + "-" + day.ToString("D2"); } private void Save() { DailyTaskSaveService.Save(saveData); } private void NotifyTasksChanged() { if (OnTasksChanged != null) { OnTasksChanged(); } } }