Story流程+本地化

This commit is contained in:
SoulliesOfficial
2026-07-24 17:56:30 -04:00
parent b0e0a7d5aa
commit de70870682
250 changed files with 5719 additions and 272966 deletions

View File

@@ -1,18 +1,19 @@
using Ichni.Menu;
using System.Collections;
using System.Collections.Generic;
using Ichni.Diagnostics;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
namespace Ichni
{
// MenuManager.Start 会直接读取已加载的设置,因此 SettingsManager 必须先完成 Start。
[DefaultExecutionOrder(-10000)]
public class SettingsManager : SerializedMonoBehaviour
{
@@ -23,11 +24,15 @@ namespace Ichni
private const string BalancedQualityName = "Balanced";
private const string HighFidelityQualityName = "High Fidelity";
/// <summary>新建存档、无效 Code 与无法迁移的旧数据统一回退到的 Locale。</summary>
public const string DefaultLocaleCode = "zh-CN";
/// <summary>
/// 与设置界面 Dropdown 顺序严格一致的 Locale Code
/// 不使用 AvailableLocales.Locales 的返回顺序,因为 Addressables Catalog 条目顺序不属于稳定存档契约。
/// 旧版 <see cref="SettingsSaveData.languageIndex"/> 与设置页面下拉框的固定对应关系
/// <para>该数组不从 Addressables Catalog 动态读取Catalog 条目顺序不稳定存档契约。</para>
/// <para>新存档不会保存数组索引,而是保存 <see cref="SettingsSaveData.languageCode"/>。保留本数组仅为迁移旧 ES3 数据与驱动当前下拉框。</para>
/// </summary>
private static readonly string[] SupportedLocaleCodes =
private static readonly string[] LegacyLanguageIndexToCode =
{
"zh-CN",
"en",
@@ -49,10 +54,10 @@ namespace Ichni
// 记录相机在玩家关闭后处理前的原始状态。重新开启时必须恢复原始值,不能无条件将所有相机改为开启。
private readonly Dictionary<int, bool> postProcessingCameraStates = new Dictionary<int, bool>();
// 语言切换请求统一由一个协程串行处理。协程运行期间的新请求只覆盖目标索引
// 语言切换请求统一由一个协程串行处理。协程运行期间的新请求只覆盖目标 Locale Code
private Coroutine applyLanguageSettingsCoroutine;
private bool isLanguageRoutineRunning;
private int pendingLanguageIndex = -1;
private string pendingLanguageCode;
private void Awake()
{
@@ -106,6 +111,7 @@ namespace Ichni
public void LoadGameSettings()
{
string savePath = Application.persistentDataPath + "/GameData/" + SettingsSaveFileName;
bool migratedLegacyLanguageIndex = false;
if (ES3.FileExists(savePath))
{
// 保留原有 ES3 Key确保本次数据模型重命名不会让已有本地设置失效。
@@ -115,10 +121,17 @@ namespace Ichni
{
settingsSaveData = CreateDefaultSettingsSaveData();
}
Debug.Log($"[SettingsManager] 已加载存档:{savePath},当前语言索引={settingsSaveData.languageIndex}");
migratedLegacyLanguageIndex = MigrateLanguageCodeIfNeeded();
ApplyAudioSettings();
ApplyGraphicSettings();
ApplyLanguageSettings();
// 旧语言索引只应参与一次迁移;立刻写回稳定 Code避免每次启动都重复依赖旧字段。
if (migratedLegacyLanguageIndex)
{
SaveGameSettings();
}
}
/// <summary>
@@ -253,6 +266,8 @@ namespace Ichni
private SettingsSaveData CreateDefaultSettingsSaveData()
{
SettingsSaveData defaultSettings = new SettingsSaveData();
defaultSettings.languageCode = DefaultLocaleCode;
defaultSettings.languageIndex = GetLanguageDropdownIndex(DefaultLocaleCode);
defaultSettings.graphicsQualityPreset = GetGraphicsQualityPreset(QualitySettings.GetQualityLevel());
#if UNITY_STANDALONE || UNITY_EDITOR
defaultSettings.displayMode = GetGraphicsDisplayMode(Screen.fullScreenMode);
@@ -346,35 +361,47 @@ namespace Ichni
}
/// <summary>
/// 将当前语言索引应用到 Unity Localization 管线
/// 返回当前存档语言在设置下拉框中的显示索引
/// <para>该索引只服务 UI持久化始终使用 <see cref="SettingsSaveData.languageCode"/>。</para>
/// </summary>
public int GetLanguageDropdownIndex()
{
return GetLanguageDropdownIndex(GetSavedLanguageCode());
}
/// <summary>
/// 由设置下拉框提交玩家的新语言选择。
/// <para>所有 UI 必须经由此方法写入语言,避免重新将易变的下拉索引保存为存档契约。</para>
/// </summary>
public void SetLanguageByDropdownIndex(int languageIndex)
{
string localeCode = GetLocaleCodeFromDropdownIndex(languageIndex);
settingsSaveData.languageCode = localeCode;
// 继续维护旧字段,便于仍在本地的旧存档、开发调试和降级检查;运行时不再读取它。
settingsSaveData.languageIndex = GetLanguageDropdownIndex(localeCode);
ApplyLanguageSettings();
}
/// <summary>
/// 将当前语言 Code 应用到 Unity Localization 管线。
/// 本方法只等待 Unity 官方 InitializationOperation并在其完成后应用玩家最新一次语言选择。
/// </summary>
public void ApplyLanguageSettings()
{
if (settingsSaveData == null)
{
BuildHangTracer.Log("SETTINGS_MGR", "[ApplyLanguageSettings] settingsSaveData 为空,取消语言切换。");
return;
}
pendingLanguageIndex = settingsSaveData.languageIndex;
pendingLanguageCode = GetSavedLanguageCode();
if (isLanguageRoutineRunning)
{
BuildHangTracer.Log(
"SETTINGS_MGR",
$"[ApplyLanguageSettings] 已有语言切换流程运行,更新待处理索引为 {pendingLanguageIndex}。");
return;
}
isLanguageRoutineRunning = true;
BuildHangTracer.Log(
"SETTINGS_MGR",
$"[ApplyLanguageSettings] 启动语言切换流程,目标索引={pendingLanguageIndex}。");
//LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale(SupportedLocaleCodes[pendingLanguageIndex]);
applyLanguageSettingsCoroutine = StartCoroutine(ApplyLanguageSettingsRoutine());
}
@@ -386,68 +413,40 @@ namespace Ichni
// 避开 Dropdown.onValueChanged 当前同步调用栈。
yield return null;
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"[Coroutine] 等待 Unity Localization InitializationOperation。" +
$"CurrentState={LocalizationBootstrap.State}");
yield return LocalizationBootstrap.WaitForInitialization();
if (!LocalizationBootstrap.IsReady)
// 直接等待 Unity Localization 官方初始化句柄。无需再维护一套平行的
// Bootstrap 状态机;当 Addressables/场景异步队列可正常推进时,该句柄会自行完成。
AsyncOperationHandle<LocalizationSettings> initializationOperation =
LocalizationSettings.InitializationOperation;
yield return initializationOperation;
if (initializationOperation.Status != AsyncOperationStatus.Succeeded)
{
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"[Coroutine] Unity Localization 初始化失败,取消语言切换。" +
$"Reason={LocalizationBootstrap.FailureReason}");
pendingLanguageIndex = -1;
if (initializationOperation.OperationException != null)
{
Debug.LogException(initializationOperation.OperationException, this);
}
Debug.LogError("[SettingsManager] Unity Localization 初始化失败,取消语言切换。", this);
pendingLanguageCode = null;
yield break;
}
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"[Coroutine] Unity Localization 已就绪。启动 Locale=" +
$"{LocalizationSettings.SelectedLocale?.Identifier.Code ?? "<null>"}");
// 处理流程运行期间收到的最新请求。
while (pendingLanguageIndex >= 0)
while (!string.IsNullOrEmpty(pendingLanguageCode))
{
int requestedLanguageIndex = pendingLanguageIndex;
pendingLanguageIndex = -1;
if (requestedLanguageIndex < 0 ||
requestedLanguageIndex >= SupportedLocaleCodes.Length)
{
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"[Coroutine] 语言索引无效。Index={requestedLanguageIndex}" +
$"SupportedCount={SupportedLocaleCodes.Length}。回退到简体中文。");
// 旧版本存档或损坏存档可能留下越界索引。将它修正为稳定的默认值,
// 避免首次启动时保持一个不受 SettingsSave 控制的 Locale。
requestedLanguageIndex = 0;
settingsSaveData.languageIndex = 0;
}
string targetLocaleCode = SupportedLocaleCodes[requestedLanguageIndex];
string targetLocaleCode = pendingLanguageCode;
pendingLanguageCode = null;
Locale targetLocale = LocalizationSettings.AvailableLocales.GetLocale(targetLocaleCode);
Locale currentLocale = LocalizationSettings.SelectedLocale;
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"[Coroutine] 准备切换 Locale" +
$"{currentLocale?.Identifier.Code ?? "<null>"} -> " +
$"{targetLocale?.Identifier.Code ?? "<null>"}");
if (targetLocale == null)
{
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"[Coroutine] Localization Settings 中不存在 Locale '{targetLocaleCode}',取消本次切换。");
Debug.LogWarning(
$"[SettingsManager] Localization Settings 中不存在 Locale '{targetLocaleCode}'。");
continue;
}
if (currentLocale == targetLocale)
{
BuildHangTracer.Log("SETTINGS_COROUTINE", "[Coroutine] 目标 Locale 与当前 Locale 相同,跳过切换。");
continue;
}
@@ -456,35 +455,90 @@ namespace Ichni
// setter 会同步更新 SelectedLocale并通知 LocalizedString / LocalizedAsset 按需刷新。
// 初始化已在本协程开头完成,因此此处不会触发同步 Locale 加载。
yield return null;
Locale appliedLocale = LocalizationSettings.SelectedLocale;
if (appliedLocale == targetLocale)
{
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"<<< [CRITICAL] SelectedLocale 已切换:{targetLocale.Identifier.Code}");
}
else
{
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"[Coroutine] SelectedLocale 校验失败。Expected={targetLocale.Identifier.Code}" +
$"Actual={appliedLocale?.Identifier.Code ?? "<null>"}");
}
}
}
finally
{
applyLanguageSettingsCoroutine = null;
isLanguageRoutineRunning = false;
BuildHangTracer.Log(
"SETTINGS_COROUTINE",
$"[Coroutine] 语言切换流程结束。PendingIndex={pendingLanguageIndex}");
}
}
/// <summary>
/// 将未保存过 Locale Code 的旧 ES3 数据迁移为稳定 Code。
/// <para>迁移使用旧版本的固定下拉排序,而不是当前 Addressables 的 Locale 列表顺序;因此旧玩家的语言不会被错误映射。</para>
/// </summary>
/// <returns>本次是否写入了新的 <see cref="SettingsSaveData.languageCode"/>。</returns>
private bool MigrateLanguageCodeIfNeeded()
{
if (!string.IsNullOrWhiteSpace(settingsSaveData.languageCode))
{
string normalizedCode = NormalizeLocaleCode(settingsSaveData.languageCode);
bool wasNormalized = normalizedCode != settingsSaveData.languageCode;
settingsSaveData.languageCode = normalizedCode;
settingsSaveData.languageIndex = GetLanguageDropdownIndex(normalizedCode);
return wasNormalized;
}
string migratedCode = GetLocaleCodeFromDropdownIndex(settingsSaveData.languageIndex);
Debug.Log($"[SettingsManager] 已将旧语言索引 {settingsSaveData.languageIndex} 迁移为 Locale Code '{migratedCode}'。", this);
settingsSaveData.languageCode = migratedCode;
settingsSaveData.languageIndex = GetLanguageDropdownIndex(migratedCode);
return true;
}
/// <summary>读取并规范化当前存档的 Locale Code。空值或未知 Code 始终回退到 <see cref="DefaultLocaleCode"/>。</summary>
private string GetSavedLanguageCode()
{
string normalizedCode = NormalizeLocaleCode(settingsSaveData.languageCode);
settingsSaveData.languageCode = normalizedCode;
settingsSaveData.languageIndex = GetLanguageDropdownIndex(normalizedCode);
return normalizedCode;
}
private static string GetLocaleCodeFromDropdownIndex(int languageIndex)
{
if (languageIndex < 0 || languageIndex >= LegacyLanguageIndexToCode.Length)
{
Debug.LogWarning($"[SettingsManager] 语言下拉索引 {languageIndex} 超出范围,回退到 {DefaultLocaleCode}。");
return DefaultLocaleCode;
}
return LegacyLanguageIndexToCode[languageIndex];
}
private static int GetLanguageDropdownIndex(string localeCode)
{
for (int index = 0; index < LegacyLanguageIndexToCode.Length; index++)
{
if (LegacyLanguageIndexToCode[index] == localeCode)
{
return index;
}
}
return 0;
}
private static string NormalizeLocaleCode(string localeCode)
{
if (string.IsNullOrWhiteSpace(localeCode))
{
return DefaultLocaleCode;
}
foreach (string supportedCode in LegacyLanguageIndexToCode)
{
if (string.Equals(supportedCode, localeCode, System.StringComparison.OrdinalIgnoreCase))
{
return supportedCode;
}
}
Debug.LogWarning($"[SettingsManager] Locale Code '{localeCode}' 不在首发语言列表中,回退到 {DefaultLocaleCode}。");
return DefaultLocaleCode;
}
/// <summary>
/// 判断本谱面是否启用全屏判定。谱面强制要求的配置优先于玩家开关。
/// </summary>