#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