Files
bansonic_beta_main/Assets/Editor/PackagingResetTool.cs
T

384 lines
14 KiB
C#
Raw 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(英雄成长)
};
[MenuItem("Tools/Packaging/一键初始化打包设置(恢复默认)", false, 0)]
public static void ResetForPackaging()
{
bool confirmed = EditorUtility.DisplayDialog(
"一键初始化打包设置",
"将执行以下重置(仅玩家进度,不改配置/解锁/技能配装):\n\n" +
"1. 玩家:金币、材料、经验、等级清零\n" +
"2. 英雄:经验、成长阶位、自动突破、等级锁、出战/完成/MVP 统计、入队日期清零\n" +
"3. 装备:删除 " + GeneratedEquipmentFolder + " 下所有生成装备,清除英雄已装备引用\n" +
"4. 商店:所有物品 purchasedCount、user_has_read 清零(限购配置 itemPurchaseQuota 不动)\n" +
"5. 清理本机测试存档(.cache_bridge 货币/成长/装备/商店存档,不含拥有权账本)与相关 PlayerPrefs\n\n" +
"此操作会修改并保存资产文件,建议在版本控制下执行。是否继续?",
"执行重置",
"取消");
if (!confirmed)
{
return;
}
var report = new StringBuilder();
int errors = 0;
try
{
AssetDatabase.StartAssetEditing();
errors += ResetPlayers(report);
errors += ResetHeroes(report);
errors += DeleteGeneratedEquipmentAssets(report);
errors += ResetStoreItems(report);
}
catch (Exception ex)
{
errors++;
report.AppendLine("资产重置阶段异常:" + ex.Message);
}
finally
{
AssetDatabase.StopAssetEditing();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
// 运行时存档 / PlayerPrefs 清理(放在资产保存之后,避免被回写覆盖)。
errors += ClearRuntimeSaves(report);
errors += ClearPlayerPrefs(report);
report.Insert(0, errors == 0
? "打包初始化完成,未发现错误。\n\n"
: "打包初始化完成,但有 " + errors + " 处警告/错误,请查看下方明细。\n\n");
Debug.Log("[PackagingReset]\n" + report);
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。");
return errors;
}
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 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;
// 商店 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.*)。");
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