603 lines
25 KiB
C#
603 lines
25 KiB
C#
using Ichni.Menu;
|
||
using Ichni.UI;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
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
|
||
{
|
||
private const string SettingsSaveFileName = "SettingsSave.json";
|
||
private const string SettingsSaveKey = "SettingsSave";
|
||
|
||
private const string PerformantQualityName = "Performant";
|
||
private const string BalancedQualityName = "Balanced";
|
||
private const string HighFidelityQualityName = "High Fidelity";
|
||
|
||
/// <summary>新建存档、无效 Code 与无法迁移的旧数据统一回退到的 Locale。</summary>
|
||
public const string DefaultLocaleCode = "zh-CN";
|
||
|
||
/// <summary>
|
||
/// 旧版 <see cref="SettingsSaveData.languageIndex"/> 与设置页面下拉框的固定对应关系。
|
||
/// <para>该数组不从 Addressables Catalog 动态读取;Catalog 条目顺序不是稳定的存档契约。</para>
|
||
/// <para>新存档不会保存数组索引,而是保存 <see cref="SettingsSaveData.languageCode"/>。保留本数组仅为迁移旧 ES3 数据与驱动当前下拉框。</para>
|
||
/// </summary>
|
||
private static readonly string[] LegacyLanguageIndexToCode =
|
||
{
|
||
"zh-CN",
|
||
"en",
|
||
"zh-TW",
|
||
"ja",
|
||
"ko",
|
||
"vi-VN",
|
||
"th"
|
||
};
|
||
|
||
public static SettingsManager instance;
|
||
|
||
[FormerlySerializedAs("settingsPage")]
|
||
public SettingsUIPage settingsUIPage;
|
||
|
||
[FormerlySerializedAs("gameSettings")]
|
||
public SettingsSaveData settingsSaveData;
|
||
|
||
// 记录相机在玩家关闭后处理前的原始状态。重新开启时必须恢复原始值,不能无条件将所有相机改为开启。
|
||
private readonly Dictionary<int, bool> postProcessingCameraStates = new Dictionary<int, bool>();
|
||
|
||
// 语言切换请求统一由一个协程串行处理。协程运行期间的新请求只覆盖目标 Locale Code。
|
||
private Coroutine applyLanguageSettingsCoroutine;
|
||
private bool isLanguageRoutineRunning;
|
||
private string pendingLanguageCode;
|
||
|
||
private void Awake()
|
||
{
|
||
if (instance == null)
|
||
{
|
||
instance = this;
|
||
|
||
DontDestroyOnLoad(gameObject);
|
||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||
RenderPipelineManager.beginCameraRendering += ApplyPostProcessingSettingBeforeCameraRender;
|
||
}
|
||
else
|
||
{
|
||
Destroy(gameObject);
|
||
}
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (instance != this)
|
||
{
|
||
return;
|
||
}
|
||
|
||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||
RenderPipelineManager.beginCameraRendering -= ApplyPostProcessingSettingBeforeCameraRender;
|
||
|
||
if (applyLanguageSettingsCoroutine != null)
|
||
{
|
||
StopCoroutine(applyLanguageSettingsCoroutine);
|
||
applyLanguageSettingsCoroutine = null;
|
||
}
|
||
|
||
isLanguageRoutineRunning = false;
|
||
instance = null;
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
LoadGameSettings();
|
||
}
|
||
|
||
[Button]
|
||
public void SaveGameSettings()
|
||
{
|
||
string savePath = Application.persistentDataPath + "/GameData/" + SettingsSaveFileName;
|
||
ES3.Save(SettingsSaveKey, settingsSaveData, savePath);
|
||
}
|
||
|
||
[Button]
|
||
public void LoadGameSettings()
|
||
{
|
||
string savePath = Application.persistentDataPath + "/GameData/" + SettingsSaveFileName;
|
||
bool migratedLegacyLanguageIndex = false;
|
||
bool migratedUiEffectsSetting = false;
|
||
if (ES3.FileExists(savePath))
|
||
{
|
||
// 保留原有 ES3 Key,确保本次数据模型重命名不会让已有本地设置失效。
|
||
settingsSaveData = ES3.Load<SettingsSaveData>(SettingsSaveKey, savePath);
|
||
}
|
||
else
|
||
{
|
||
settingsSaveData = CreateDefaultSettingsSaveData();
|
||
}
|
||
|
||
migratedLegacyLanguageIndex = MigrateLanguageCodeIfNeeded();
|
||
migratedUiEffectsSetting = MigrateUiEffectsSettingIfNeeded();
|
||
ApplyAudioSettings();
|
||
ApplyGraphicSettings();
|
||
ApplyLanguageSettings();
|
||
|
||
// 旧语言索引只应参与一次迁移;立刻写回稳定 Code,避免每次启动都重复依赖旧字段。
|
||
if (migratedLegacyLanguageIndex || migratedUiEffectsSetting)
|
||
{
|
||
SaveGameSettings();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将音频分类的当前设置写入 Wwise RTPC。
|
||
/// </summary>
|
||
public void ApplyAudioSettings()
|
||
{
|
||
AkUnitySoundEngine.SetRTPCValue("MasterVolume", settingsSaveData.masterVolume);
|
||
AkUnitySoundEngine.SetRTPCValue("MusicVolume", settingsSaveData.musicVolume);
|
||
AkUnitySoundEngine.SetRTPCValue("SoundFXVolume", settingsSaveData.soundEffectVolume);
|
||
AkUnitySoundEngine.SetRTPCValue("UIVolume", settingsSaveData.uiVolume);
|
||
ApplyInGameAudioEffectsSettings();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用游戏内音乐特效开关。关闭时立即清除歌曲正在使用的高通与低通滤波,
|
||
/// 防止设置变更后残留上一帧的 RTPC 值。
|
||
/// </summary>
|
||
public void ApplyInGameAudioEffectsSettings()
|
||
{
|
||
if (settingsSaveData.enableInGameAudioEffects)
|
||
{
|
||
return;
|
||
}
|
||
|
||
GameManager.Instance?.songPlayer?.ResetInGameAudioEffects();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将现有图形设置应用到运行时。更细的 Quality、VSync 与平台策略会在 Graphics 分类重构时接入。
|
||
/// </summary>
|
||
public void ApplyGraphicSettings()
|
||
{
|
||
int qualityLevelIndex = GetQualityLevelIndex(settingsSaveData.graphicsQualityPreset);
|
||
if (QualitySettings.GetQualityLevel() != qualityLevelIndex)
|
||
{
|
||
QualitySettings.SetQualityLevel(qualityLevelIndex, true);
|
||
}
|
||
|
||
Application.targetFrameRate = settingsSaveData.targetFrame;
|
||
UniversalRenderPipelineAsset currentUrpAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
||
currentUrpAsset.renderScale = 0.5f + 0.1f * settingsSaveData.renderScaleLevel;
|
||
ApplyDisplaySettings();
|
||
ApplyPostProcessingSettings();
|
||
ApplyUiEffectsSettings();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用后处理总开关与 Bloom 档位。
|
||
/// 关闭总开关时,已存在的 URP 相机会直接关闭 <c>renderPostProcessing</c>,并关闭 GameScene 的 Global Volume。
|
||
/// 这样既不会执行原生后处理,也不会执行 ScriptablePostProcessor 的自定义 Render Pass。
|
||
/// </summary>
|
||
public void ApplyPostProcessingSettings()
|
||
{
|
||
bool enablePostProcessing = settingsSaveData.enablePostProcessing;
|
||
UniversalAdditionalCameraData[] cameraDataList =
|
||
FindObjectsByType<UniversalAdditionalCameraData>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
|
||
foreach (UniversalAdditionalCameraData cameraData in cameraDataList)
|
||
{
|
||
int instanceId = cameraData.GetInstanceID();
|
||
if (!postProcessingCameraStates.ContainsKey(instanceId))
|
||
{
|
||
postProcessingCameraStates.Add(instanceId, cameraData.renderPostProcessing);
|
||
}
|
||
|
||
cameraData.renderPostProcessing = enablePostProcessing && postProcessingCameraStates[instanceId];
|
||
}
|
||
|
||
if (PostProcessingManager.Instance != null)
|
||
{
|
||
PostProcessingManager.Instance.ApplySettings(enablePostProcessing, settingsSaveData.bloomQuality);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用 UI 特效总开关。
|
||
/// <para>
|
||
/// 仅处理显式挂载 <see cref="UiExpandingContoursEffect"/> 的装饰性 UI Image,而不修改共享材质本身。
|
||
/// 这样关闭特效时会移除对应 Shader 的绘制开销,同时保留页面布局和其它 UI 元素。
|
||
/// </para>
|
||
/// <para>
|
||
/// 使用 Include 是为了让当前处于隐藏状态的 SongSelectionPage、SettingsPage 也同步保存目标状态;
|
||
/// 它们下次显示时无需再经过一帧特效可见的过渡。
|
||
/// </para>
|
||
/// </summary>
|
||
public void ApplyUiEffectsSettings()
|
||
{
|
||
if (settingsSaveData == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UiExpandingContoursEffect[] effectTargets =
|
||
FindObjectsByType<UiExpandingContoursEffect>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
|
||
foreach (UiExpandingContoursEffect effectTarget in effectTargets)
|
||
{
|
||
effectTarget.ApplyEnabledState(settingsSaveData.enableUiEffects);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理场景切换后新加载的相机与 Global Volume。SettingsManager 会跨场景保留,
|
||
/// 因此不能只在首次读取存档时应用一次图形设置。
|
||
/// </summary>
|
||
private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
|
||
{
|
||
if (settingsSaveData != null)
|
||
{
|
||
ApplyPostProcessingSettings();
|
||
ApplyUiEffectsSettings();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 兜底处理运行时晚于场景加载创建的相机(例如 Theme 或演出对象中的相机)。
|
||
/// 仅在玩家关闭后处理时执行,保证该相机在首次渲染前也不会运行后处理。
|
||
/// </summary>
|
||
private void ApplyPostProcessingSettingBeforeCameraRender(ScriptableRenderContext context, Camera camera)
|
||
{
|
||
if (settingsSaveData == null || settingsSaveData.enablePostProcessing ||
|
||
!camera.TryGetComponent(out UniversalAdditionalCameraData cameraData))
|
||
{
|
||
return;
|
||
}
|
||
|
||
int instanceId = cameraData.GetInstanceID();
|
||
if (!postProcessingCameraStates.ContainsKey(instanceId))
|
||
{
|
||
postProcessingCameraStates.Add(instanceId, cameraData.renderPostProcessing);
|
||
}
|
||
|
||
cameraData.renderPostProcessing = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 VSync、窗口模式和分辨率立即应用到 Standalone / Unity Editor。
|
||
/// 移动端由系统管理显示模式与分辨率,因此不执行这些设置。
|
||
/// </summary>
|
||
public void ApplyDisplaySettings()
|
||
{
|
||
#if UNITY_STANDALONE || UNITY_EDITOR
|
||
QualitySettings.vSyncCount = settingsSaveData.vSyncEnabled ? 1 : 0;
|
||
|
||
if (settingsSaveData.displayResolutionWidth <= 0 || settingsSaveData.displayResolutionHeight <= 0)
|
||
{
|
||
settingsSaveData.displayResolutionWidth = Screen.width;
|
||
settingsSaveData.displayResolutionHeight = Screen.height;
|
||
}
|
||
|
||
Screen.SetResolution(settingsSaveData.displayResolutionWidth, settingsSaveData.displayResolutionHeight,
|
||
GetFullScreenMode(settingsSaveData.displayMode));
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 首次创建设置存档时,继承 Project Settings 为当前平台指定的默认 Quality Level。
|
||
/// 之后玩家的选择由 SettingsSaveData 持久化,不会在下次启动时被平台默认值覆盖。
|
||
/// </summary>
|
||
private SettingsSaveData CreateDefaultSettingsSaveData()
|
||
{
|
||
SettingsSaveData defaultSettings = new SettingsSaveData();
|
||
defaultSettings.uiEffectsSettingInitialized = true;
|
||
defaultSettings.languageCode = DefaultLocaleCode;
|
||
defaultSettings.languageIndex = GetLanguageDropdownIndex(DefaultLocaleCode);
|
||
defaultSettings.graphicsQualityPreset = GetGraphicsQualityPreset(QualitySettings.GetQualityLevel());
|
||
#if UNITY_STANDALONE || UNITY_EDITOR
|
||
defaultSettings.displayMode = GetGraphicsDisplayMode(Screen.fullScreenMode);
|
||
defaultSettings.displayResolutionWidth = Screen.width;
|
||
defaultSettings.displayResolutionHeight = Screen.height;
|
||
#endif
|
||
return defaultSettings;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将旧版 SettingsSave 中不存在的 UI 特效开关迁移为默认开启。
|
||
/// <para>
|
||
/// 不提升 Schema Version:项目尚未发布,且本字段只补齐一个布尔默认值。迁移标记会随即写回 ES3,
|
||
/// 之后玩家主动关闭 UI 效果时不会被再次覆盖。
|
||
/// </para>
|
||
/// </summary>
|
||
private bool MigrateUiEffectsSettingIfNeeded()
|
||
{
|
||
if (settingsSaveData.uiEffectsSettingInitialized)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
settingsSaveData.enableUiEffects = true;
|
||
settingsSaveData.uiEffectsSettingInitialized = true;
|
||
return true;
|
||
}
|
||
|
||
private FullScreenMode GetFullScreenMode(GraphicsDisplayMode displayMode)
|
||
{
|
||
return displayMode switch
|
||
{
|
||
GraphicsDisplayMode.Fullscreen => FullScreenMode.ExclusiveFullScreen,
|
||
GraphicsDisplayMode.Windowed => FullScreenMode.Windowed,
|
||
_ => FullScreenMode.FullScreenWindow
|
||
};
|
||
}
|
||
|
||
private GraphicsDisplayMode GetGraphicsDisplayMode(FullScreenMode fullScreenMode)
|
||
{
|
||
return fullScreenMode switch
|
||
{
|
||
FullScreenMode.ExclusiveFullScreen => GraphicsDisplayMode.Fullscreen,
|
||
FullScreenMode.Windowed => GraphicsDisplayMode.Windowed,
|
||
_ => GraphicsDisplayMode.BorderlessFullscreen
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将存档枚举映射到 Project Settings 中同名的 Quality Level。
|
||
/// 若配置缺失,回退到 Balanced;这是移动端的首发默认档位。
|
||
/// </summary>
|
||
private int GetQualityLevelIndex(GraphicsQualityPreset qualityPreset)
|
||
{
|
||
string qualityName = GetQualityName(qualityPreset);
|
||
string[] qualityNames = QualitySettings.names;
|
||
for (int index = 0; index < qualityNames.Length; index++)
|
||
{
|
||
if (qualityNames[index] == qualityName)
|
||
{
|
||
return index;
|
||
}
|
||
}
|
||
|
||
Debug.LogWarning($"[SettingsManager] 未找到 Quality Level '{qualityName}',回退至 Balanced。");
|
||
return FindQualityLevelOrFallback(BalancedQualityName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将当前 Quality Level 索引转换为可保存的枚举。未知的自定义档位统一按 Balanced 保存。
|
||
/// </summary>
|
||
private GraphicsQualityPreset GetGraphicsQualityPreset(int qualityLevelIndex)
|
||
{
|
||
string[] qualityNames = QualitySettings.names;
|
||
if (qualityLevelIndex < 0 || qualityLevelIndex >= qualityNames.Length)
|
||
{
|
||
return GraphicsQualityPreset.Balanced;
|
||
}
|
||
|
||
return qualityNames[qualityLevelIndex] switch
|
||
{
|
||
PerformantQualityName => GraphicsQualityPreset.Performant,
|
||
HighFidelityQualityName => GraphicsQualityPreset.HighFidelity,
|
||
_ => GraphicsQualityPreset.Balanced
|
||
};
|
||
}
|
||
|
||
private int FindQualityLevelOrFallback(string qualityName)
|
||
{
|
||
string[] qualityNames = QualitySettings.names;
|
||
for (int index = 0; index < qualityNames.Length; index++)
|
||
{
|
||
if (qualityNames[index] == qualityName)
|
||
{
|
||
return index;
|
||
}
|
||
}
|
||
|
||
return Mathf.Clamp(QualitySettings.GetQualityLevel(), 0, qualityNames.Length - 1);
|
||
}
|
||
|
||
private string GetQualityName(GraphicsQualityPreset qualityPreset)
|
||
{
|
||
return qualityPreset switch
|
||
{
|
||
GraphicsQualityPreset.Performant => PerformantQualityName,
|
||
GraphicsQualityPreset.HighFidelity => HighFidelityQualityName,
|
||
_ => BalancedQualityName
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回当前存档语言在设置下拉框中的显示索引。
|
||
/// <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)
|
||
{
|
||
return;
|
||
}
|
||
|
||
pendingLanguageCode = GetSavedLanguageCode();
|
||
|
||
if (isLanguageRoutineRunning)
|
||
{
|
||
return;
|
||
}
|
||
|
||
isLanguageRoutineRunning = true;
|
||
applyLanguageSettingsCoroutine = StartCoroutine(ApplyLanguageSettingsRoutine());
|
||
}
|
||
|
||
private IEnumerator ApplyLanguageSettingsRoutine()
|
||
{
|
||
// try/finally 确保协程正常结束、被 StopCoroutine 或发生异常时,运行状态都会被清除。
|
||
try
|
||
{
|
||
// 避开 Dropdown.onValueChanged 当前同步调用栈。
|
||
yield return null;
|
||
|
||
// 直接等待 Unity Localization 官方初始化句柄。无需再维护一套平行的
|
||
// Bootstrap 状态机;当 Addressables/场景异步队列可正常推进时,该句柄会自行完成。
|
||
AsyncOperationHandle<LocalizationSettings> initializationOperation =
|
||
LocalizationSettings.InitializationOperation;
|
||
yield return initializationOperation;
|
||
if (initializationOperation.Status != AsyncOperationStatus.Succeeded)
|
||
{
|
||
if (initializationOperation.OperationException != null)
|
||
{
|
||
Debug.LogException(initializationOperation.OperationException, this);
|
||
}
|
||
|
||
Debug.LogError("[SettingsManager] Unity Localization 初始化失败,取消语言切换。", this);
|
||
pendingLanguageCode = null;
|
||
yield break;
|
||
}
|
||
|
||
// 处理流程运行期间收到的最新请求。
|
||
while (!string.IsNullOrEmpty(pendingLanguageCode))
|
||
{
|
||
string targetLocaleCode = pendingLanguageCode;
|
||
pendingLanguageCode = null;
|
||
Locale targetLocale = LocalizationSettings.AvailableLocales.GetLocale(targetLocaleCode);
|
||
Locale currentLocale = LocalizationSettings.SelectedLocale;
|
||
|
||
if (targetLocale == null)
|
||
{
|
||
Debug.LogWarning(
|
||
$"[SettingsManager] Localization Settings 中不存在 Locale '{targetLocaleCode}'。");
|
||
continue;
|
||
}
|
||
|
||
if (currentLocale == targetLocale)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
LocalizationSettings.SelectedLocale = targetLocale;
|
||
|
||
// setter 会同步更新 SelectedLocale,并通知 LocalizedString / LocalizedAsset 按需刷新。
|
||
// 初始化已在本协程开头完成,因此此处不会触发同步 Locale 加载。
|
||
yield return null;
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
applyLanguageSettingsCoroutine = null;
|
||
isLanguageRoutineRunning = false;
|
||
}
|
||
}
|
||
|
||
/// <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>
|
||
public bool ShouldUseFullScreenJudge()
|
||
{
|
||
return settingsSaveData.judgeType || InformationTransistor.instance?.difficulty?.ForceFullScreenJudge == true;
|
||
}
|
||
}
|
||
}
|