using System; using System.Collections.Generic; using UnityEngine; [Serializable] public class ChartFileEntry { [Header("Inspector")] public int difficulty; public float difficultyLEVEL; [Header("Inspector")] public float enemyTotalHP_Multiplier; [Header("Inspector")] public string difficultyName; [Header("Inspector")] public TextAsset chartFile; [Header("Inspector")] [TextArea(3, 10)] public string difficultyDescription; [Header("Inspector")] public int lastScoreForThisDifficulty; [Header("Inspector")] public int chartPersonalRecordForThisDifficulty; public int idolPersonalRecordForThisDifficulty; public int totalPersonalRecordForThisDifficulty; [Header("Inspector")] public float levelProgressForThisDifficulty; [Header("Inspector")] public DateTime lastPlayTimeForThisDifficulty; [Header("Inspector")] public int experienceProvidedForThisDifficulty; } [CreateAssetMenu(fileName = "NewSongData", menuName = "EaseOut/SongData")] public class SongData : ScriptableObject { public string usernameID = "user_TestUser@FloatGaming"; public string songName; public int songID; public int songLength_second; public string artistName; public string painter; public string level_creator; public string belongsTo_whichDLC; public int bpm; public float songDuration; public int game_enterTimes; public float time_totalPlayingTime; public DateTime uploadData; [Header("Inspector")] public Sprite illustration; public Sprite backgroudPIC; public Sprite fullscreen_songPicture; public Sprite card_profileImage; [Header("Inspector")] public Sprite right_pic_bottom_image; public Sprite right_pic_top_image; [Header("Inspector")] public AudioClip audioFile; public AudioClip audio_previewAudio; [Header("Inspector")] public int difficultyID; public Dictionary difficultyNumberMap = new Dictionary(); public Dictionary personalRecordMap = new Dictionary(); public Dictionary idolRecordMap = new Dictionary(); public Dictionary chartScoreMap = new Dictionary(); public Dictionary chartFileMap = new Dictionary(); public List chartFiles = new List(); public int personalRecord; public float current_levelProgress; public bool _if_level_is_EASE; public int max_comboRecord; [Header("Inspector")] public int thisLevel_selectedDifficultyID; [Header("Inspector")] public string thisLevel_addDateString; [Header("Inspector")] [TextArea(3, 10)] public string thisLevel_overallDescription; [Header("Enemy Configuration")] [Tooltip("List of enemy IDs for this song (0-5 enemies)")] public List enemyList = new List(); private void OnValidate() { if (enemyList != null && enemyList.Count > 5) { enemyList.RemoveRange(5, enemyList.Count - 5); Debug.LogWarning($"[SongData] {songName}: enemyList length restricted to a maximum of 5."); } } private void OnEnable() { if (difficultyNumberMap == null) difficultyNumberMap = new Dictionary(); if (personalRecordMap == null) personalRecordMap = new Dictionary(); if (idolRecordMap == null) idolRecordMap = new Dictionary(); if (chartScoreMap == null) chartScoreMap = new Dictionary(); if (chartFileMap == null) chartFileMap = new Dictionary(); if (chartFiles == null) chartFiles = new List(); SyncRecordsFromChartFiles(); SyncChartFileMap(); } public int CalculateHighestPersonalRecord() { if (personalRecordMap == null) personalRecordMap = new Dictionary(); if (idolRecordMap == null) idolRecordMap = new Dictionary(); int highest = 0; foreach (var kvp in personalRecordMap) { int diff = kvp.Key; int p = kvp.Value; int i = idolRecordMap.ContainsKey(diff) ? idolRecordMap[diff] : 0; int combined = p + i; if (combined > highest) highest = combined; } personalRecord = highest; return highest; } /// /// 将数据保存到持久化存储,并附加 MD5 校验和防止篡改。 /// public void SavePersistent() { try { string path = System.IO.Path.Combine(Application.persistentDataPath, $"SongData_{songID}.json"); SongDataSerializable serializable = new SongDataSerializable(this); // 1. 序列化基础数据 string jsonWithoutHash = JsonUtility.ToJson(serializable); // 2. 计算 MD5 (使用自定义盐值提高安全性) serializable.dataHash = CalculateMD5(jsonWithoutHash + "bansonic2026@fudongyouxi"); // 3. 再次序列化包含 Hash 的最终数据 string finalJson = JsonUtility.ToJson(serializable, true); System.IO.File.WriteAllText(path, finalJson); #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); UnityEditor.AssetDatabase.SaveAssets(); } #endif Debug.Log($"[SongData] {songName} saved with hash validation."); } catch (System.Exception e) { Debug.LogError($"[SongData] Save failed: {e.Message}"); } } /// /// 从持久化 JSON 恢复数据。 /// 校验逻辑已迁移至 DataCheck 脚本,此处仅执行基础加载。 /// public void LoadPersistent() { string path = System.IO.Path.Combine(Application.persistentDataPath, $"SongData_{songID}.json"); if (System.IO.File.Exists(path)) { try { string json = System.IO.File.ReadAllText(path); // 尝试解析数据 SongDataSerializable serializable = JsonUtility.FromJson(json); if (serializable != null) { serializable.ApplyTo(this); // Debug.Log($"[SongData] {songName} data restored."); } } catch (System.Exception e) { Debug.LogWarning($"[SongData] {songName} 基础加载跳过: {e.Message}"); } } } /// /// 当检测到篡改或文件损坏时,重置所有动态记录并立即重新保存文件以恢复合法性。 /// private void ResetToZeroAndSave() { // 1. 重置所有动态数值字段 game_enterTimes = 0; time_totalPlayingTime = 0; current_levelProgress = 0; _if_level_is_EASE = false; max_comboRecord = 0; personalRecord = 0; // 2. 清空并确保初始化所有动态字典映射 if (difficultyNumberMap == null) difficultyNumberMap = new Dictionary(); else difficultyNumberMap.Clear(); if (personalRecordMap == null) personalRecordMap = new Dictionary(); else personalRecordMap.Clear(); if (idolRecordMap == null) idolRecordMap = new Dictionary(); else idolRecordMap.Clear(); if (chartScoreMap == null) chartScoreMap = new Dictionary(); else chartScoreMap.Clear(); // 3. 立即调用保存方法,生成一个新的、带有正确哈希值的合法文件 SavePersistent(); Debug.LogWarning($"[SongData] {songName} has been fully reset to zero due to data integrity failure."); } /// /// 静态校验方法,可在异步线程调用,判断 JSON 内容是否合法。 /// public static bool VerifyJsonIntegrity(string json, string salt, out SongDataSerializable result) { result = null; try { SongDataSerializable serializable = JsonUtility.FromJson(json); if (serializable == null) return false; string savedHash = serializable.dataHash; serializable.dataHash = ""; string currentJson = JsonUtility.ToJson(serializable); // 使用静态 MD5 计算 string calculatedHash = StaticCalculateMD5(currentJson + salt); result = serializable; return savedHash == calculatedHash; } catch { return false; } } public static string StaticCalculateMD5(string input) { using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) { byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(input); byte[] hashBytes = md5.ComputeHash(inputBytes); return System.BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } } private string CalculateMD5(string input) { return StaticCalculateMD5(input); } public void UpdateDifficultyRecord(int difficulty, int pmScore, int idolScore) { if (personalRecordMap == null) personalRecordMap = new Dictionary(); if (idolRecordMap == null) idolRecordMap = new Dictionary(); int currentBestTotal = GetPersonalRecord(difficulty) + GetIdolRecord(difficulty); int newTotal = pmScore + idolScore; if (newTotal <= currentBestTotal) return; personalRecordMap[difficulty] = pmScore; idolRecordMap[difficulty] = idolScore; ChartFileEntry entry = chartFiles != null ? chartFiles.Find(e => e != null && e.difficulty == difficulty) : null; if (entry != null) { entry.chartPersonalRecordForThisDifficulty = pmScore; entry.idolPersonalRecordForThisDifficulty = idolScore; entry.totalPersonalRecordForThisDifficulty = newTotal; } CalculateHighestPersonalRecord(); } public void GetAbsoluteHighestScore(out int highestScore, out int bestDifficulty, out string bestDifficultyName) { highestScore = 0; bestDifficulty = -1; bestDifficultyName = ""; if (chartFiles != null) { for (int i = 0; i < chartFiles.Count; i++) { ChartFileEntry entry = chartFiles[i]; if (entry == null) continue; int total = GetPersonalRecord(entry.difficulty) + GetIdolRecord(entry.difficulty); if (total <= 0) total = entry.totalPersonalRecordForThisDifficulty; if (total <= 0) total = entry.chartPersonalRecordForThisDifficulty + entry.idolPersonalRecordForThisDifficulty; if (total > highestScore || bestDifficulty == -1) { highestScore = total; bestDifficulty = entry.difficulty; bestDifficultyName = !string.IsNullOrEmpty(entry.difficultyName) ? entry.difficultyName : GetDifficultyDefaultName(entry.difficulty); } } } if (bestDifficulty != -1 || personalRecordMap == null) return; foreach (var kvp in personalRecordMap) { int diff = kvp.Key; int total = kvp.Value + GetIdolRecord(diff); if (total > highestScore || bestDifficulty == -1) { highestScore = total; bestDifficulty = diff; bestDifficultyName = GetDifficultyDefaultName(diff); } } } private string GetDifficultyDefaultName(int difficulty) { switch (difficulty) { case 0: return "Easy"; case 1: return "Hard"; case 2: return "Incredible"; case 3: return "Impossible"; default: return "Unknown"; } } public void UpdatePersonalRecord(int difficulty, int newScore) { if (personalRecordMap == null) personalRecordMap = new Dictionary(); if (!personalRecordMap.ContainsKey(difficulty) || newScore > personalRecordMap[difficulty]) personalRecordMap[difficulty] = newScore; ChartFileEntry entry = chartFiles != null ? chartFiles.Find(e => e != null && e.difficulty == difficulty) : null; if (entry != null) { entry.chartPersonalRecordForThisDifficulty = personalRecordMap[difficulty]; int idol = idolRecordMap != null && idolRecordMap.ContainsKey(difficulty) ? idolRecordMap[difficulty] : 0; entry.totalPersonalRecordForThisDifficulty = personalRecordMap[difficulty] + idol; } CalculateHighestPersonalRecord(); } public void UpdateIdolRecord(int difficulty, int newIdolScore) { if (idolRecordMap == null) idolRecordMap = new Dictionary(); if (!idolRecordMap.ContainsKey(difficulty) || newIdolScore > idolRecordMap[difficulty]) idolRecordMap[difficulty] = newIdolScore; ChartFileEntry entry = chartFiles != null ? chartFiles.Find(e => e != null && e.difficulty == difficulty) : null; if (entry != null) { entry.idolPersonalRecordForThisDifficulty = idolRecordMap[difficulty]; int personal = personalRecordMap != null && personalRecordMap.ContainsKey(difficulty) ? personalRecordMap[difficulty] : 0; entry.totalPersonalRecordForThisDifficulty = personal + idolRecordMap[difficulty]; } CalculateHighestPersonalRecord(); } public void UpdateChartScore(int difficulty, int newChartScore) { if (chartScoreMap == null) chartScoreMap = new Dictionary(); chartScoreMap[difficulty] = newChartScore; ChartFileEntry entry = chartFiles != null ? chartFiles.Find(e => e != null && e.difficulty == difficulty) : null; if (entry != null) entry.lastScoreForThisDifficulty = newChartScore; } public int GetPersonalRecord(int difficulty) { if (personalRecordMap == null) return 0; return personalRecordMap.TryGetValue(difficulty, out int v) ? v : 0; } public int GetIdolRecord(int difficulty) { if (idolRecordMap == null) return 0; return idolRecordMap.TryGetValue(difficulty, out int v) ? v : 0; } public int GetChartScore(int difficulty) { if (chartScoreMap == null) return 0; return chartScoreMap.TryGetValue(difficulty, out int v) ? v : 0; } public TextAsset GetChartFile(int difficulty) { if (chartFiles != null) { for (int i = 0; i < chartFiles.Count; i++) { ChartFileEntry entry = chartFiles[i]; if (entry != null && entry.difficulty == difficulty) return entry.chartFile; } } if (chartFileMap != null && chartFileMap.TryGetValue(difficulty, out TextAsset mapped)) return mapped; return null; } public void SyncChartFileMap() { if (chartFileMap == null) chartFileMap = new Dictionary(); chartFileMap.Clear(); if (chartFiles == null) return; for (int i = 0; i < chartFiles.Count; i++) { ChartFileEntry entry = chartFiles[i]; if (entry != null && entry.chartFile != null) chartFileMap[entry.difficulty] = entry.chartFile; } } public void SyncRecordsFromChartFiles() { if (personalRecordMap == null) personalRecordMap = new Dictionary(); if (idolRecordMap == null) idolRecordMap = new Dictionary(); if (chartScoreMap == null) chartScoreMap = new Dictionary(); if (chartFiles == null) return; for (int i = 0; i < chartFiles.Count; i++) { ChartFileEntry entry = chartFiles[i]; if (entry == null) continue; if (!difficultyNumberMap.ContainsKey(entry.difficulty)) difficultyNumberMap[entry.difficulty] = Mathf.RoundToInt(entry.difficultyLEVEL); if (!personalRecordMap.ContainsKey(entry.difficulty) || entry.chartPersonalRecordForThisDifficulty > personalRecordMap[entry.difficulty]) personalRecordMap[entry.difficulty] = entry.chartPersonalRecordForThisDifficulty; if (!idolRecordMap.ContainsKey(entry.difficulty) || entry.idolPersonalRecordForThisDifficulty > idolRecordMap[entry.difficulty]) idolRecordMap[entry.difficulty] = entry.idolPersonalRecordForThisDifficulty; if (!chartScoreMap.ContainsKey(entry.difficulty) || entry.lastScoreForThisDifficulty > chartScoreMap[entry.difficulty]) chartScoreMap[entry.difficulty] = entry.lastScoreForThisDifficulty; int p = personalRecordMap.ContainsKey(entry.difficulty) ? personalRecordMap[entry.difficulty] : 0; int idol = idolRecordMap.ContainsKey(entry.difficulty) ? idolRecordMap[entry.difficulty] : 0; entry.totalPersonalRecordForThisDifficulty = p + idol; } CalculateHighestPersonalRecord(); } }