#if UNITY_EDITOR using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; /// /// 构建前防线:若随包发布的玩家进度数据(Player_SO / AllyHero_SO / SongData) /// 仍带有非零测试值,直接中断构建。 /// /// 目的:杜绝"忘记跑一键初始化 → 测试存档随安装包发布 → 玩家首启被灌入 999988 /// 金币等测试数据"的事故(这是历史上多次出现的存档污染根因)。 /// /// 只做只读校验,不改动任何资产;发现问题时抛出 BuildFailedException 并给出 /// 明确指引(跑 Tools/Packaging/一键初始化打包设置)。 /// public sealed class PackagingBuildGuard : IPreprocessBuildWithReport { public int callbackOrder => 0; public void OnPreprocessBuild(BuildReport report) { var problems = new List(); CheckPlayers(problems); CheckPlayerSkills(problems); CheckHeroes(problems); CheckSongs(problems); CheckMails(problems); if (problems.Count == 0) { return; } var sb = new StringBuilder(); sb.AppendLine("检测到随包发布的玩家进度数据仍带测试值,已中断构建。"); sb.AppendLine("请先执行菜单:Tools/Packaging/一键初始化打包设置(恢复默认),再重新打包。"); sb.AppendLine(); sb.AppendLine("问题明细:"); int shown = 0; foreach (var p in problems) { sb.AppendLine(" - " + p); if (++shown >= 40) { sb.AppendLine(" - ...(其余 " + (problems.Count - shown) + " 项省略)"); break; } } Debug.LogError("[PackagingBuildGuard]\n" + sb); throw new BuildFailedException(sb.ToString()); } private static void CheckPlayers(List problems) { string[] intFields = { "player_coins", "player_material", "player_currentEXP", "player_currentLevel", "commonExpBottle78001", "mediumExpBottle78002", "superiorExpBottle78003", "supremeExpBottle78004", "extraordinaryExpBottle78005", "celestialExpBottle78006", "dushMaterial78021", "dushMaterial78022", "dushMaterial78023", "dushMaterial78024", }; foreach (var guid in AssetDatabase.FindAssets("t:Player_SO")) { string path = AssetDatabase.GUIDToAssetPath(guid); var so = AssetDatabase.LoadAssetAtPath(path); if (so == null) continue; var s = new SerializedObject(so); foreach (var f in intFields) { var prop = s.FindProperty(f); if (prop != null && prop.intValue != 0) { problems.Add(path + " :: " + f + " = " + prop.intValue); } } var rank = s.FindProperty("uRankingScore"); if (rank != null && Mathf.Abs(rank.floatValue) > 0.0001f) { problems.Add(path + " :: uRankingScore = " + rank.floatValue); } } PlayerExperienceRuntimeProbe expProbe; if (SecureSaveVault.TryLoadJson("player_experience", "runtime", out expProbe) && expProbe != null && expProbe.playerExp != 0) { problems.Add("player_experience/runtime :: playerExp = " + expProbe.playerExp); } int backupExp; if (PlayerProgressBackupService.TryRestorePlayerExperience(out backupExp) && backupExp != 0) { problems.Add("player progress backup :: playerExperience = " + backupExp); } } [System.Serializable] private sealed class PlayerExperienceRuntimeProbe { public int playerExp; } private static void CheckHeroes(List problems) { string[] intFields = { "ally_currentEXP", "ally_growthUnlockedTierIndex", "ally_battleDeployCount", "ally_finishCount", "ally_mvpCount", }; foreach (var guid in AssetDatabase.FindAssets("t:AllyHero_SO")) { string path = AssetDatabase.GUIDToAssetPath(guid); var so = AssetDatabase.LoadAssetAtPath(path); if (so == null) continue; var s = new SerializedObject(so); foreach (var f in intFields) { var prop = s.FindProperty(f); if (prop != null && prop.intValue != 0) { problems.Add(path + " :: " + f + " = " + prop.intValue); } } var join = s.FindProperty("ally_joinDateUtcTicks"); if (join != null && join.longValue != 0L) { problems.Add(path + " :: ally_joinDateUtcTicks = " + join.longValue); } } } private static void CheckPlayerSkills(List problems) { foreach (var guid in AssetDatabase.FindAssets("t:userLevel_skills_SO")) { string path = AssetDatabase.GUIDToAssetPath(guid); var so = AssetDatabase.LoadAssetAtPath(path); if (so == null || so.skills == null || so.skills.Count == 0) { continue; } int enabledCount = 0; int enabledIndex = -1; for (int i = 0; i < so.skills.Count; i++) { var entry = so.skills[i]; if (entry != null && entry.isEnabled) { enabledCount++; enabledIndex = i; } } if (enabledCount != 1 || enabledIndex != 0) { problems.Add(path + " :: player skill selection must ship with only index 0 enabled, current enabledIndex=" + enabledIndex + ", enabledCount=" + enabledCount); } } PlayerSkillSaveData runtimeSkillData; if (SecureSaveVault.TryLoadJson("player_skills", "runtime", out runtimeSkillData) && runtimeSkillData != null) { if (runtimeSkillData.selectedSkillIndex != 0 || runtimeSkillData.postMatchRewardCounter != 0 || runtimeSkillData.skillSwitchCooldownRemainingMatches != 0) { problems.Add("player_skills/runtime :: selectedSkillIndex=" + runtimeSkillData.selectedSkillIndex + ", postMatchRewardCounter=" + runtimeSkillData.postMatchRewardCounter + ", skillSwitchCooldownRemainingMatches=" + runtimeSkillData.skillSwitchCooldownRemainingMatches); } } PlayerSkillSaveData backupSkillData; if (PlayerProgressBackupService.TryRestorePlayerSkill(out backupSkillData) && backupSkillData != null) { if (backupSkillData.selectedSkillIndex != 0 || backupSkillData.postMatchRewardCounter != 0 || backupSkillData.skillSwitchCooldownRemainingMatches != 0) { problems.Add("player progress backup :: playerSkill selectedSkillIndex=" + backupSkillData.selectedSkillIndex + ", postMatchRewardCounter=" + backupSkillData.postMatchRewardCounter + ", skillSwitchCooldownRemainingMatches=" + backupSkillData.skillSwitchCooldownRemainingMatches); } } } private static void CheckSongs(List problems) { string[] recordFields = { "lastScoreForThisDifficulty", "chartPersonalRecordForThisDifficulty", "idolPersonalRecordForThisDifficulty", "totalPersonalRecordForThisDifficulty", }; foreach (var guid in AssetDatabase.FindAssets("t:SongData")) { string path = AssetDatabase.GUIDToAssetPath(guid); var so = AssetDatabase.LoadAssetAtPath(path); if (so == null) continue; var s = new SerializedObject(so); var enter = s.FindProperty("game_enterTimes"); if (enter != null && enter.intValue != 0) { problems.Add(path + " :: game_enterTimes = " + enter.intValue); } var chartFiles = s.FindProperty("chartFiles"); if (chartFiles != null && chartFiles.isArray) { for (int i = 0; i < chartFiles.arraySize; i++) { var entry = chartFiles.GetArrayElementAtIndex(i); foreach (var f in recordFields) { var prop = entry.FindPropertyRelative(f); if (prop != null && prop.intValue != 0) { problems.Add(path + " :: chartFiles[" + i + "]." + f + " = " + prop.intValue); } } } } } } private static void CheckMails(List problems) { foreach (var guid in AssetDatabase.FindAssets("t:mail_so")) { string path = AssetDatabase.GUIDToAssetPath(guid); var so = AssetDatabase.LoadAssetAtPath(path); if (so == null) continue; // 跳过 mail_id = 0 的模板文件。 if (so.mail_id == 0) continue; // 标记未领取(isReceived:0)且携带奖励的邮件 → 新玩家首启会自动领取。 if (!so.isReceived && so.rewardList != null && so.rewardList.Count > 0) { int totalRewards = 0; foreach (var reward in so.rewardList) { totalRewards += reward.reward_ammount; } if (totalRewards > 0) { problems.Add(path + " :: mail_id=" + so.mail_id + " 未领取且含 " + so.rewardList.Count + " 项奖励(新玩家会自动领取)"); } } } } } #endif