Files
bansonic_beta_main/Assets/Editor/PackagingResetTool.cs
2026-07-23 23:16:18 +08:00

578 lines
22 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
/// <summary>
/// 打包前一键初始化工具:把随包发布的玩家进度状态恢复到默认(零)值。
///
/// 只重置“玩家进度状态”,绝不改动业务/配置数据。具体范围见菜单确认框。
/// 明确不碰:isUnlocked(角色/歌曲/剧情解锁)、itemPurchaseQuota(限购配置)、
/// equippedSkillGroupIDs(技能配装)、associatedAllyHero 等引用、DLC 配置、
/// 拥有权账本 .own.dat、以及技能/羁绊/皮肤等定义资产。
/// </summary>
public static class PackagingResetTool
{
private const string GeneratedEquipmentFolder = "Assets/Resources/so/uEquip";
private const string NextGeneratedIdPrefsKey = "bansonic_equipment_next_id_v1";
// .cache_bridge 下按固定文件名持久化的本机测试存档(不进包,但编辑器 Play 会回写 .asset)。
// 注意:故意不含 .own.dat / .own.bak(拥有权账本),以免影响解锁业务逻辑。
private static readonly string[] CacheBridgeSaveFiles =
{
".eco.dat", ".eco.bak", ".eco.tmp", // PlayerEconomyStorage(货币)
".ahd.dat", ".ahd.bak", ".ahd.tmp", // AllyHeroDeployLedgerStorage(英雄成长)
".xpv.dat", ".xpv.bak", ".xpv.tmp", // ExpBottleLedgerStorage(经验瓶)
".dsm.dat", ".dsm.bak", ".dsm.tmp", // DushMaterialLedgerStorage(突破材料)
".eqc.dat", ".eqc.bak", ".eqc.tmp", // EquipmentConsumableLedgerStorage(装备消耗品)
};
[MenuItem("Tools/Packaging/一键初始化打包设置(恢复默认)", false, 0)]
public static void ResetForPackaging()
{
bool isBatchMode = Application.isBatchMode;
if (!isBatchMode)
{
bool confirmed = EditorUtility.DisplayDialog(
"一键初始化打包设置",
"将执行以下重置(仅玩家进度,不改配置/解锁/技能配装):\n\n" +
"1. 玩家:金币、材料、经验、等级清零\n" +
"2. 英雄:经验、成长阶位、自动突破、等级锁、出战/完成/MVP 统计、入队日期清零\n" +
"2.5 歌曲:游玩次数、累计时长、各难度成绩记录清零(歌曲解锁/谱面配置不动)\n" +
"3. 装备:删除 " + GeneratedEquipmentFolder + " 下所有生成装备,清除英雄已装备引用\n" +
"4. 商店:所有物品 purchasedCount、user_has_read 清零(限购配置 itemPurchaseQuota 不动)\n" +
"5. 清理本机测试存档(.cache_bridge 货币/成长/经验瓶/突破材料/装备消耗品、player_rks、player_experience、player_skills、装备/商店存档,不含拥有权账本)与相关 PlayerPrefs\n\n" +
"此操作会修改并保存资产文件,建议在版本控制下执行。是否继续?",
"执行重置",
"取消");
if (!confirmed)
{
return;
}
}
else
{
Debug.Log("[PackagingReset] BatchMode 检测到,跳过确认弹窗,直接执行重置。");
}
var report = new StringBuilder();
int errors = 0;
// 【关键】先删除运行时存档,再清理 .asset。
// 原因:SongData.OnEnable() 在编辑器模式下会从存档回写到 .asset;
// 如果先改 .asset 再删存档,.asset 被修改时触发 OnEnable() → 从存档回写 → .asset 又脏了。
errors += ClearRuntimeSaves(report);
errors += ClearPlayerPrefs(report);
try
{
AssetDatabase.StartAssetEditing();
errors += ResetPlayers(report);
errors += ResetPlayerSkills(report);
errors += ResetHeroes(report);
errors += ResetSongs(report);
errors += DeleteGeneratedEquipmentAssets(report);
errors += ResetStoreItems(report);
}
catch (Exception ex)
{
errors++;
report.AppendLine("资产重置阶段异常:" + ex.Message);
}
finally
{
AssetDatabase.StopAssetEditing();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
report.Insert(0, errors == 0
? "打包初始化完成,未发现错误。\n\n"
: "打包初始化完成,但有 " + errors + " 处警告/错误,请查看下方明细。\n\n");
Debug.Log("[PackagingReset]\n" + report);
if (!isBatchMode)
{
EditorUtility.DisplayDialog(
errors == 0 ? "初始化完成" : "初始化完成(有警告)",
report.ToString(),
"好");
}
}
private static int ResetPlayers(StringBuilder report)
{
int errors = 0;
int count = 0;
foreach (var guid in AssetDatabase.FindAssets("t:Player_SO"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<Player_SO>(path);
if (so == null)
{
continue;
}
var serialized = new SerializedObject(so);
SetIntIfPresent(serialized, "player_coins", 0);
SetIntIfPresent(serialized, "player_material", 0);
SetIntIfPresent(serialized, "player_currentEXP", 0);
SetIntIfPresent(serialized, "player_currentLevel", 0);
SetFloatIfPresent(serialized, "uRankingScore", 0f);
// 经验瓶 / 突破材料等消耗品库存也一并清零(属于玩家进度,非配置)。
SetIntIfPresent(serialized, "commonExpBottle78001", 0);
SetIntIfPresent(serialized, "mediumExpBottle78002", 0);
SetIntIfPresent(serialized, "superiorExpBottle78003", 0);
SetIntIfPresent(serialized, "supremeExpBottle78004", 0);
SetIntIfPresent(serialized, "extraordinaryExpBottle78005", 0);
SetIntIfPresent(serialized, "celestialExpBottle78006", 0);
SetIntIfPresent(serialized, "dushMaterial78021", 0);
SetIntIfPresent(serialized, "dushMaterial78022", 0);
SetIntIfPresent(serialized, "dushMaterial78023", 0);
SetIntIfPresent(serialized, "dushMaterial78024", 0);
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(so);
count++;
}
report.AppendLine("玩家资产已重置:" + count + " 个 Player_SO。");
try
{
SecureSaveVault.Delete("player_skills", "runtime");
PlayerProgressBackupService.ClearPlayerSkillBackup();
report.AppendLine("已清理 player_skills 运行时存档与备份(player_skills/runtime)。");
}
catch (Exception ex)
{
errors++;
report.AppendLine("清理 player_skills 存档失败:" + ex.Message);
}
return errors;
}
private static int ResetPlayerSkills(StringBuilder report)
{
int count = 0;
foreach (var guid in AssetDatabase.FindAssets("t:userLevel_skills_SO"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<userLevel_skills_SO>(path);
if (so == null || so.skills == null || so.skills.Count == 0)
{
continue;
}
var serialized = new SerializedObject(so);
var skills = serialized.FindProperty("skills");
if (skills != null && skills.isArray)
{
for (int i = 0; i < skills.arraySize; i++)
{
var entry = skills.GetArrayElementAtIndex(i);
SetChildBool(entry, "isEnabled", i == 0);
}
}
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(so);
count++;
}
report.AppendLine("玩家技能资产已重置:" + count + " 个 userLevel_skills_SO(仅启用第 0 个技能)。");
return 0;
}
private static int ResetHeroes(StringBuilder report)
{
int count = 0;
foreach (var guid in AssetDatabase.FindAssets("t:AllyHero_SO"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<AllyHero_SO>(path);
if (so == null)
{
continue;
}
var serialized = new SerializedObject(so);
// 成长进度(不动 isUnlocked / equippedSkillGroupIDs)。
SetIntIfPresent(serialized, "ally_currentEXP", 0);
SetIntIfPresent(serialized, "ally_growthUnlockedTierIndex", 0);
SetBoolIfPresent(serialized, "ally_autoBreakthroughEnabled", false);
SetBoolIfPresent(serialized, "level_lock", false);
// 战斗统计。
SetIntIfPresent(serialized, "ally_battleDeployCount", 0);
SetIntIfPresent(serialized, "ally_finishCount", 0);
SetIntIfPresent(serialized, "ally_mvpCount", 0);
SetLongIfPresent(serialized, "ally_joinDateUtcTicks", 0L);
// 装备引用:因为要删除全部生成装备,这里同步清空引用避免悬空。
SetObjectIfPresent(serialized, "equippedEquipment", null);
SetStringIfPresent(serialized, "equippedEquipmentId", string.Empty);
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(so);
count++;
}
report.AppendLine("英雄资产已重置:" + count + " 个 AllyHero_SO(经验/阶位/统计/装备引用)。");
return 0;
}
private static int ResetSongs(StringBuilder report)
{
int count = 0;
int charts = 0;
foreach (var guid in AssetDatabase.FindAssets("t:SongData"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<SongData>(path);
if (so == null)
{
continue;
}
var serialized = new SerializedObject(so);
// 顶层游玩进度(不动 songID / isUnlocked / bpm / 谱面配置等)。
SetStringIfPresent(serialized, "usernameID", string.Empty);
SetIntIfPresent(serialized, "game_enterTimes", 0);
SetFloatIfPresent(serialized, "time_totalPlayingTime", 0f);
// 逐难度成绩记录(chartFiles 数组内),只清成绩,保留难度/敌人/谱面配置。
var chartFiles = serialized.FindProperty("chartFiles");
if (chartFiles != null && chartFiles.isArray)
{
for (int i = 0; i < chartFiles.arraySize; i++)
{
var entry = chartFiles.GetArrayElementAtIndex(i);
SetChildInt(entry, "lastScoreForThisDifficulty", 0);
SetChildInt(entry, "chartPersonalRecordForThisDifficulty", 0);
SetChildInt(entry, "idolPersonalRecordForThisDifficulty", 0);
SetChildInt(entry, "totalPersonalRecordForThisDifficulty", 0);
SetChildFloat(entry, "levelProgressForThisDifficulty", 0f);
charts++;
}
}
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(so);
// 【关键】清空内存中的运行时数据映射,防止 OnEnable/SavePersistent 回写脏数据。
// SongData 在编辑器模式下会从存档加载到内存 mappersonalRecordMap 等),
// 再通过 SyncEntriesFromMaps() 同步到 chartFiles[] 序列化字段。
// 即使磁盘上的存档和 .asset 都清零了,内存中的 map 如果还有旧值,
// Unity 保存资产时会把内存脏数据写回磁盘。必须显式清空。
so.ClearSaveDataAndReload();
count++;
}
report.AppendLine("歌曲资产已重置:" + count + " 个 SongData(游玩次数/时长/" + charts + " 条难度成绩记录)。");
return 0;
}
private static void SetChildInt(SerializedProperty parent, string field, int value)
{
var prop = parent.FindPropertyRelative(field);
if (prop != null)
{
prop.intValue = value;
}
}
private static void SetChildFloat(SerializedProperty parent, string field, float value)
{
var prop = parent.FindPropertyRelative(field);
if (prop != null)
{
prop.floatValue = value;
}
}
private static void SetChildBool(SerializedProperty parent, string field, bool value)
{
var prop = parent.FindPropertyRelative(field);
if (prop != null)
{
prop.boolValue = value;
}
}
private static int DeleteGeneratedEquipmentAssets(StringBuilder report)
{
if (!AssetDatabase.IsValidFolder(GeneratedEquipmentFolder))
{
report.AppendLine("装备目录不存在,跳过删除:" + GeneratedEquipmentFolder);
return 0;
}
int errors = 0;
int deleted = 0;
foreach (var guid in AssetDatabase.FindAssets("t:equipmentSO", new[] { GeneratedEquipmentFolder }))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (AssetDatabase.DeleteAsset(path))
{
deleted++;
}
else
{
errors++;
report.AppendLine("删除装备失败:" + path);
}
}
report.AppendLine("已删除生成装备资产:" + deleted + " 个(目录 " + GeneratedEquipmentFolder + ")。");
return errors;
}
private static int ResetStoreItems(StringBuilder report)
{
int count = 0;
foreach (var guid in AssetDatabase.FindAssets("t:storeItemSO"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<storeItemSO>(path);
if (so == null)
{
continue;
}
var serialized = new SerializedObject(so);
// 只清运行时计数,不动 itemPurchaseQuota / isOnShelf / 关联引用等配置。
SetIntIfPresent(serialized, "purchasedCount", 0);
SetBoolIfPresent(serialized, "user_has_read", false);
serialized.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(so);
count++;
}
report.AppendLine("商店物品已重置:" + count + " 个 storeItemSOpurchasedCount / user_has_read 清零)。");
return 0;
}
private static int ClearRuntimeSaves(StringBuilder report)
{
int errors = 0;
// 歌曲成绩存档(必须先删除,避免回写覆盖已清零的 .asset)。
int songSavesCleared = 0;
foreach (var guid in AssetDatabase.FindAssets("t:SongData"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<SongData>(path);
if (so == null || so.songID <= 0)
{
continue;
}
try
{
string legacySongPath = Path.Combine(Application.persistentDataPath, $"SongData_{so.songID}.json");
SecureSaveVault.Delete("song_runtime", so.songID.ToString(), legacySongPath);
songSavesCleared++;
}
catch (Exception ex)
{
errors++;
report.AppendLine($"清理歌曲 {so.songID} 存档失败:" + ex.Message);
}
}
report.AppendLine("已清理歌曲运行时存档:" + songSavesCleared + " 个(song_runtime/*)。");
// 商店 purchasedCount 存档(SecureSaveVault + 旧版明文文件)。
try
{
string legacyStorePath = Path.Combine(Application.persistentDataPath, "storeSystem_state.json");
SecureSaveVault.Delete("store_state", "runtime", legacyStorePath);
report.AppendLine("已清理商店运行时存档(store_state/runtime)。");
}
catch (Exception ex)
{
errors++;
report.AppendLine("清理商店存档失败:" + ex.Message);
}
// 生成装备运行时存档。
try
{
SecureSaveVault.Delete("runtime_equipment", "generated_list");
report.AppendLine("已清理生成装备运行时存档(runtime_equipment/generated_list)。");
}
catch (Exception ex)
{
errors++;
report.AppendLine("清理装备存档失败:" + ex.Message);
}
// 货币 / 英雄成长的独立加密存档(按固定文件名)。
string cacheBridge = Path.Combine(Application.persistentDataPath, ".cache_bridge");
int fileDeleted = 0;
foreach (var fileName in CacheBridgeSaveFiles)
{
try
{
string full = Path.Combine(cacheBridge, fileName);
if (File.Exists(full))
{
File.Delete(full);
fileDeleted++;
}
}
catch (Exception ex)
{
errors++;
report.AppendLine("删除存档文件失败(" + fileName + "):" + ex.Message);
}
}
report.AppendLine("已清理 .cache_bridge 货币/成长存档文件:" + fileDeleted + " 个(保留拥有权账本 .own.*)。");
// player_rks / player_experience 等 SecureSaveVault 存档。
try
{
SecureSaveVault.Delete("player_rks", "overall");
report.AppendLine("已清理 player_rks 运行时存档(player_rks/overall)。");
}
catch (Exception ex)
{
errors++;
report.AppendLine("清理 player_rks 存档失败:" + ex.Message);
}
try
{
string legacyExpPath = Path.Combine(Application.persistentDataPath, "player_experience.json");
SecureSaveVault.Delete("player_experience", "runtime", legacyExpPath);
PlayerProgressBackupService.ClearPlayerExperienceBackup();
report.AppendLine("已清理 player_experience 运行时存档(player_experience/runtime)。");
}
catch (Exception ex)
{
errors++;
report.AppendLine("清理 player_experience 存档失败:" + ex.Message);
}
return errors;
}
private static int ClearPlayerPrefs(StringBuilder report)
{
PlayerPrefs.DeleteKey(NextGeneratedIdPrefsKey);
int equipKeysCleared = 0;
int debtKeysCleared = 0;
foreach (var guid in AssetDatabase.FindAssets("t:AllyHero_SO"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<AllyHero_SO>(path);
if (so == null)
{
continue;
}
var serialized = new SerializedObject(so);
var idProp = serialized.FindProperty("ally_heroID");
if (idProp == null)
{
continue;
}
int heroId = idProp.intValue;
// 已装备装备的本机记录会指向被删装备,需清理(不动技能配装记录)。
string equipKey = "ally_equippedEquipment_" + heroId;
if (PlayerPrefs.HasKey(equipKey))
{
PlayerPrefs.DeleteKey(equipKey);
equipKeysCleared++;
}
// 升阶欠账镜像键(idolUpgrade 用):属于成长进度残留。
string debtKey = "idol_upgrade_pending_debt_" + heroId;
if (PlayerPrefs.HasKey(debtKey))
{
PlayerPrefs.DeleteKey(debtKey);
debtKeysCleared++;
}
}
// 队伍槽位经验镜像键(selected_heroSlot0X_exp)。不动 _heroID 槽位编队本身,
// 仅把已归零英雄对应的槽位经验一并清零,避免下次启动被读回覆盖 .asset。
int slotExpCleared = 0;
for (int slot = 1; slot <= 5; slot++)
{
string expKey = "selected_heroSlot0" + slot + "_exp";
if (PlayerPrefs.HasKey(expKey))
{
PlayerPrefs.DeleteKey(expKey);
slotExpCleared++;
}
}
PlayerPrefs.Save();
report.AppendLine("已清理 PlayerPrefsnext-id 键 + " + equipKeysCleared + " 个已装备记录 + "
+ debtKeysCleared + " 个升阶欠账键 + " + slotExpCleared + " 个槽位经验键。");
return 0;
}
private static void SetIntIfPresent(SerializedObject so, string field, int value)
{
var prop = so.FindProperty(field);
if (prop != null)
{
prop.intValue = value;
}
}
private static void SetLongIfPresent(SerializedObject so, string field, long value)
{
var prop = so.FindProperty(field);
if (prop != null)
{
prop.longValue = value;
}
}
private static void SetFloatIfPresent(SerializedObject so, string field, float value)
{
var prop = so.FindProperty(field);
if (prop != null)
{
prop.floatValue = value;
}
}
private static void SetBoolIfPresent(SerializedObject so, string field, bool value)
{
var prop = so.FindProperty(field);
if (prop != null)
{
prop.boolValue = value;
}
}
private static void SetStringIfPresent(SerializedObject so, string field, string value)
{
var prop = so.FindProperty(field);
if (prop != null)
{
prop.stringValue = value;
}
}
private static void SetObjectIfPresent(SerializedObject so, string field, UnityEngine.Object value)
{
var prop = so.FindProperty(field);
if (prop != null)
{
prop.objectReferenceValue = value;
}
}
}
#endif