diff --git a/Assets/AddressableAssetsData/AddressableAssetSettings.asset b/Assets/AddressableAssetsData/AddressableAssetSettings.asset index a6039db1d..cc9024ca9 100644 --- a/Assets/AddressableAssetsData/AddressableAssetSettings.asset +++ b/Assets/AddressableAssetsData/AddressableAssetSettings.asset @@ -15,7 +15,7 @@ MonoBehaviour: m_DefaultGroup: 09546aca93a801441859af2a811fe53b m_currentHash: serializedVersion: 2 - Hash: ae2a22805ec7ff55174c38995aa3e54d + Hash: 00000000000000000000000000000000 m_OptimizeCatalogSize: 0 m_BuildRemoteCatalog: 0 m_BundleLocalCatalog: 0 @@ -71,6 +71,7 @@ MonoBehaviour: - {fileID: 11400000, guid: 089fc86f714d6a94bafea3b117fbd639, type: 2} - {fileID: 11400000, guid: 1c592abe924a856439461f3785ec53da, type: 2} - {fileID: 11400000, guid: 26fd03652560e7f4abec092229fc38f2, type: 2} + - {fileID: 11400000, guid: 1051221ba75fcea47b67648acedc5383, type: 2} m_BuildSettings: m_CompileScriptsInVirtualMode: 0 m_CleanupStreamingAssetsAfterBuilds: 1 @@ -121,6 +122,7 @@ MonoBehaviour: - Locale-pt-BR - Locale-ru - Locale-es + - Typography m_SchemaTemplates: [] m_GroupTemplateObjects: - {fileID: 11400000, guid: 0c158052680a3e94184c450d00291514, type: 2} diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_BundledAssetGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_BundledAssetGroupSchema.asset new file mode 100644 index 000000000..173896dcc --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_BundledAssetGroupSchema.asset @@ -0,0 +1,45 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3} + m_Name: Shared_Typography_BundledAssetGroupSchema + m_EditorClassIdentifier: + m_Group: {fileID: 11400000, guid: 1051221ba75fcea47b67648acedc5383, type: 2} + m_InternalBundleIdMode: 1 + m_Compression: 1 + m_IncludeAddressInCatalog: 1 + m_IncludeGUIDInCatalog: 1 + m_IncludeLabelsInCatalog: 1 + m_InternalIdNamingMode: 0 + m_CacheClearBehavior: 0 + m_IncludeInBuild: 1 + m_BundledAssetProviderType: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider + m_ForceUniqueProvider: 0 + m_UseAssetBundleCache: 1 + m_UseAssetBundleCrc: 1 + m_UseAssetBundleCrcForCachedBundles: 1 + m_UseUWRForLocalBundles: 0 + m_Timeout: 0 + m_ChunkedTransfer: 0 + m_RedirectLimit: -1 + m_RetryCount: 0 + m_BuildPath: + m_Id: 2ae6a7549097a9b4caefe44b61b1fcc5 + m_LoadPath: + m_Id: 631ec5c677ab8084fb9be9b9ee39c6ef + m_BundleMode: 0 + m_AssetBundleProviderType: + m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider + m_BundleNaming: 0 + m_AssetLoadMode: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_BundledAssetGroupSchema.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_BundledAssetGroupSchema.asset.meta new file mode 100644 index 000000000..e44344ce0 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_BundledAssetGroupSchema.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b39cbcc4de75ad6468842c36c7e5138d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_ContentUpdateGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_ContentUpdateGroupSchema.asset new file mode 100644 index 000000000..243801e1f --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_ContentUpdateGroupSchema.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3} + m_Name: Shared_Typography_ContentUpdateGroupSchema + m_EditorClassIdentifier: + m_Group: {fileID: 11400000, guid: 1051221ba75fcea47b67648acedc5383, type: 2} + m_StaticContent: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_ContentUpdateGroupSchema.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_ContentUpdateGroupSchema.asset.meta new file mode 100644 index 000000000..7d62ae353 --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/Shared_Typography_ContentUpdateGroupSchema.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6bd7e9bcac1e24042b79a54f47f52106 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/AssetGroups/Shared_Typography.asset b/Assets/AddressableAssetsData/AssetGroups/Shared_Typography.asset new file mode 100644 index 000000000..ca7b2a01a --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Shared_Typography.asset @@ -0,0 +1,49 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3} + m_Name: Shared_Typography + m_EditorClassIdentifier: + m_GroupName: Shared_Typography + m_Data: + m_SerializedData: [] + m_GUID: 7663edc0b4aef67468c672b641025c85 + m_SerializeEntries: + - m_GUID: e41dce1e1d6e0564b95054b5c8b8bb4c + m_Address: Typography/Profile/ZhHans + m_ReadOnly: 0 + m_SerializedLabels: + - Typography + FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 67cd590df91846b4d81698d68426e616 + m_Address: Typography/Profile/Japanese + m_ReadOnly: 0 + m_SerializedLabels: + - Typography + FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: d53382c947b5edc4a88216c5fcf37b38 + m_Address: Typography/Profile/Latin + m_ReadOnly: 0 + m_SerializedLabels: + - Typography + FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: f82150d4b378dc441884eb47d4f67b7c + m_Address: Typography/Profile/Russian + m_ReadOnly: 0 + m_SerializedLabels: + - Typography + FlaggedDuringContentUpdateRestriction: 0 + m_ReadOnly: 0 + m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} + m_SchemaSet: + m_Schemas: + - {fileID: 11400000, guid: b39cbcc4de75ad6468842c36c7e5138d, type: 2} + - {fileID: 11400000, guid: 6bd7e9bcac1e24042b79a54f47f52106, type: 2} diff --git a/Assets/AddressableAssetsData/AssetGroups/Shared_Typography.asset.meta b/Assets/AddressableAssetsData/AssetGroups/Shared_Typography.asset.meta new file mode 100644 index 000000000..eb392819c --- /dev/null +++ b/Assets/AddressableAssetsData/AssetGroups/Shared_Typography.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1051221ba75fcea47b67648acedc5383 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Tests/TypographyContractTests.cs b/Assets/Editor/Tests/TypographyContractTests.cs new file mode 100644 index 000000000..19bdd2f25 --- /dev/null +++ b/Assets/Editor/Tests/TypographyContractTests.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AibisDream.Framework; +using AibisDream.EditorTools; +using AibisDream.UI; +using NUnit.Framework; +using TMPro; +using UnityEditor; +using UnityEditor.AddressableAssets; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace AibisDream.SystemEditor.Tests +{ + public sealed class TypographyContractTests + { + private const string ProfileFolder = "Assets/GameContent/Common/Typography/Profiles"; + + [TestCase(LocaleKind.Cn, TypographyProfileKind.ZhHans)] + [TestCase(LocaleKind.Ja, TypographyProfileKind.Japanese)] + [TestCase(LocaleKind.En, TypographyProfileKind.Latin)] + [TestCase(LocaleKind.Es, TypographyProfileKind.Latin)] + [TestCase(LocaleKind.PtBr, TypographyProfileKind.Latin)] + [TestCase(LocaleKind.Ru, TypographyProfileKind.Russian)] + public void LocaleMapping_UsesExpectedProfile(LocaleKind locale, TypographyProfileKind expected) + { + Assert.That(TypographyLocaleMapping.GetProfileKind(locale), Is.EqualTo(expected)); + Assert.That( + TypographyLocaleMapping.GetProfileKey(locale), + Is.EqualTo($"Typography/Profile/{expected}")); + } + + [Test] + public void Resolve_MissingRoleFallsBackToBody() + { + TMP_FontAsset font = AssetDatabase.LoadAssetAtPath( + "Assets/Font/Assets/ChillBitmap_16px SDF.asset"); + var profile = ScriptableObject.CreateInstance(); + try + { + var body = new TypographyStyle(TypographyRole.Body, font); + profile.Configure(TypographyProfileKind.Latin, new[] { body }); + Assert.That(profile.Resolve(TypographyRole.Dialogue), Is.SameAs(body)); + } + finally + { + UnityEngine.Object.DestroyImmediate(profile); + } + } + + [Test] + public void Binding_ReapplyUsesCapturedBaselineWithoutAccumulatingMetrics() + { + TMP_FontAsset font = AssetDatabase.LoadAssetAtPath( + "Assets/Font/Assets/ChillBitmap_16px SDF.asset"); + var profile = ScriptableObject.CreateInstance(); + var go = new GameObject("TypographyMetricContract", typeof(RectTransform)); + try + { + TextMeshProUGUI text = go.AddComponent(); + text.fontSize = 40f; + text.lineSpacing = 3f; + text.characterSpacing = 2f; + LocalizedTypography binding = go.AddComponent(); + profile.Configure( + TypographyProfileKind.Latin, + new[] + { + new TypographyStyle( + TypographyRole.Body, + font, + fontSizeMultiplier: 0.75f, + lineSpacingOffset: 1f, + characterSpacingOffset: 2f) + }); + + binding.ApplyProfile(profile); + binding.ApplyProfile(profile); + + Assert.That(text.fontSize, Is.EqualTo(30f)); + Assert.That(text.lineSpacing, Is.EqualTo(4f)); + Assert.That(text.characterSpacing, Is.EqualTo(4f)); + } + finally + { + UnityEngine.Object.DestroyImmediate(go); + UnityEngine.Object.DestroyImmediate(profile); + } + } + + [Test] + public void FirstRoundProfiles_AreCompleteAndAddressableByConvention() + { + foreach (TypographyProfileKind kind in Enum.GetValues(typeof(TypographyProfileKind))) + { + TypographyProfile profile = AssetDatabase.LoadAssetAtPath( + $"{ProfileFolder}/{kind}.asset"); + Assert.That(profile, Is.Not.Null, kind.ToString()); + Assert.That(profile.ProfileKind, Is.EqualTo(kind)); + Assert.That(profile.GetValidationErrors(), Is.Empty, kind.ToString()); + Assert.That( + Enum.GetValues(typeof(TypographyRole)).Cast() + .All(role => profile.Resolve(role) != null), + Is.True, + kind.ToString()); + + string guid = AssetDatabase.AssetPathToGUID($"{ProfileFolder}/{kind}.asset"); + var entry = AddressableAssetSettingsDefaultObject.Settings.FindAssetEntry(guid); + Assert.That(entry, Is.Not.Null, kind.ToString()); + Assert.That(entry.address, Is.EqualTo($"Typography/Profile/{kind}")); + } + } + + [Test] + public void TerminalPrefabs_BindEveryTextExactlyOnce() + { + foreach (string path in TerminalTypographyMigration.PrefabPaths) + { + GameObject prefab = AssetDatabase.LoadAssetAtPath(path); + Assert.That(prefab, Is.Not.Null, path); + + TMP_Text[] texts = prefab.GetComponentsInChildren(true); + Assert.That(texts, Is.Not.Empty, path); + foreach (TMP_Text text in texts) + { + LocalizedTypography[] bindings = text.GetComponents(); + Assert.That(bindings, Has.Length.EqualTo(1), $"{path}: {text.name}"); + Assert.That(bindings[0].Role, Is.EqualTo(TypographyRole.Terminal), + $"{path}: {text.name}"); + } + } + } + + [Test] + public void PersistenceTerminalComposition_InheritsTypographyBindings() + { + Scene scene = SceneManager.GetSceneByPath(TerminalTypographyMigration.PersistenceScenePath); + bool openedForTest = !scene.IsValid() || !scene.isLoaded; + if (openedForTest) + { + scene = EditorSceneManager.OpenScene( + TerminalTypographyMigration.PersistenceScenePath, + OpenSceneMode.Additive); + } + try + { + Component[] hosts = scene.GetRootGameObjects() + .SelectMany(root => root.GetComponentsInChildren(true)) + .Where(component => component is TerminalPanel || component is InGameTerminalPanel) + .ToArray(); + Assert.That(hosts, Has.Length.EqualTo(2)); + + var violations = new List(); + foreach (Component host in hosts) + { + foreach (TMP_Text text in host.GetComponentsInChildren(true)) + { + LocalizedTypography[] bindings = text.GetComponents(); + if (bindings.Length != 1) + { + violations.Add($"{host.name}: {text.name} has {bindings.Length} bindings"); + continue; + } + + if (bindings[0].Role != TypographyRole.Terminal) + violations.Add($"{host.name}: {text.name} uses {bindings[0].Role}"); + } + } + + Assert.That(violations, Is.Empty, string.Join("\n", violations)); + } + finally + { + if (openedForTest) + EditorSceneManager.CloseScene(scene, true); + } + } + + } +} diff --git a/Assets/Editor/Tests/TypographyContractTests.cs.meta b/Assets/Editor/Tests/TypographyContractTests.cs.meta new file mode 100644 index 000000000..ef973e199 --- /dev/null +++ b/Assets/Editor/Tests/TypographyContractTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 895c6fa68b9b4ed4286362a258247f4a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Typography.meta b/Assets/Editor/Typography.meta new file mode 100644 index 000000000..292dfef8c --- /dev/null +++ b/Assets/Editor/Typography.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1ccca9c8cbbc67e47be376cc9db7ce55 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Typography/TerminalTypographyMigration.cs b/Assets/Editor/Typography/TerminalTypographyMigration.cs new file mode 100644 index 000000000..ed74090c6 --- /dev/null +++ b/Assets/Editor/Typography/TerminalTypographyMigration.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using AibisDream.Framework; +using AibisDream.UI; +using TMPro; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace AibisDream.EditorTools +{ + public static class TerminalTypographyMigration + { + public const string PrefabFolder = "Assets/GameContent/Feature_MainUI/Prefabs"; + public const string PersistenceScenePath = "Assets/Scenes/Persistence.unity"; + + // Sources must be migrated before their consumers so existing added-component + // overrides can be replaced by the component inherited from the source prefab. + public static readonly IReadOnlyList PrefabPaths = new[] + { + $"{PrefabFolder}/Common/Select Item.prefab", + $"{PrefabFolder}/Common/Slider Item.prefab", + $"{PrefabFolder}/Common/TerminalButton.prefab", + $"{PrefabFolder}/Common/TerminalChapterCard.prefab", + $"{PrefabFolder}/Common/TerminalHeader.prefab", + $"{PrefabFolder}/Common/TerminalMenuItem.prefab", + $"{PrefabFolder}/Common/TerminalOptionItem.prefab", + $"{PrefabFolder}/Common/TerminalRecordItem.prefab", + $"{PrefabFolder}/Common/TerminalSliderItem.prefab", + $"{PrefabFolder}/Common/TerminalChapterPanel.prefab", + $"{PrefabFolder}/TerminalChapterPanel.prefab", + $"{PrefabFolder}/TerminalLoadingPanel.prefab", + $"{PrefabFolder}/TerminalRecordPanel.prefab", + $"{PrefabFolder}/TerminalSettingPanel.prefab", + $"{PrefabFolder}/TerminalSettingPanelMini.prefab", + $"{PrefabFolder}/TerminalStartPanel.prefab" + }; + + [MenuItem("Tools/AIBIS Dream/Typography/Migrate Terminal Prefabs")] + public static void MigrateTerminalPrefabs() + { + int textCount = 0; + int addedCount = 0; + int removedOverrideCount = 0; + + foreach (string path in PrefabPaths) + { + MigrationResult result = MigratePrefab(path); + textCount += result.TextCount; + addedCount += result.AddedCount; + removedOverrideCount += result.RemovedOverrideCount; + } + + int sceneBindingCount = MigratePersistenceScene(); + AssetDatabase.SaveAssets(); + Debug.Log( + $"[TerminalTypographyMigration] Migrated {PrefabPaths.Count} prefabs and " + + $"{textCount} TMP texts. Added {addedCount} bindings; removed " + + $"{removedOverrideCount} redundant nested overrides; added " + + $"{sceneBindingCount} scene-owned bindings."); + } + + public static void MigrateFromCommandLine() + { + try + { + MigrateTerminalPrefabs(); + } + catch (Exception exception) + { + Debug.LogException(exception); + throw; + } + } + + private static MigrationResult MigratePrefab(string path) + { + GameObject root = PrefabUtility.LoadPrefabContents(path); + try + { + TMP_Text[] texts = root.GetComponentsInChildren(true); + if (texts.Length == 0) + throw new InvalidOperationException($"Terminal prefab contains no TMP text: {path}"); + + int addedCount = 0; + int removedOverrideCount = 0; + foreach (TMP_Text text in texts) + { + int removedForText = RemoveRedundantNestedOverrides(text); + removedOverrideCount += removedForText; + + LocalizedTypography[] bindings = text.GetComponents(); + if (bindings.Length == 0) + { + // Saving the removed override makes the source component visible + // again when this consumer prefab is next loaded. + if (removedForText > 0) + continue; + + LocalizedTypography binding = text.gameObject.AddComponent(); + binding.ConfigureForEditor(TypographyRole.Terminal); + addedCount++; + } + else if (bindings.Length == 1) + { + ConfigureIfOwnedByCurrentPrefab(bindings[0]); + } + else + { + throw new InvalidOperationException( + $"TMP text has multiple typography bindings after cleanup: {path} / {text.name}"); + } + } + + PrefabUtility.SaveAsPrefabAsset(root, path); + return new MigrationResult(texts.Length, addedCount, removedOverrideCount); + } + finally + { + PrefabUtility.UnloadPrefabContents(root); + } + } + + private static int MigratePersistenceScene() + { + Scene scene = SceneManager.GetSceneByPath(PersistenceScenePath); + bool openedForMigration = !scene.IsValid() || !scene.isLoaded; + if (openedForMigration) + scene = EditorSceneManager.OpenScene(PersistenceScenePath, OpenSceneMode.Additive); + try + { + int addedCount = 0; + var visited = new HashSet(); + foreach (GameObject root in scene.GetRootGameObjects()) + { + Component[] hosts = root.GetComponentsInChildren(true); + foreach (Component host in hosts) + { + if (host is not TerminalPanel && host is not InGameTerminalPanel) + continue; + + foreach (TMP_Text text in host.GetComponentsInChildren(true)) + { + if (!visited.Add(text)) + continue; + + LocalizedTypography[] bindings = text.GetComponents(); + if (bindings.Length == 1) + continue; + if (bindings.Length > 1) + { + throw new InvalidOperationException( + $"Scene TMP text has multiple typography bindings: {text.name}"); + } + if (PrefabUtility.IsPartOfPrefabInstance(text)) + { + throw new InvalidOperationException( + $"Terminal prefab instance is missing its source binding: {text.name}"); + } + + LocalizedTypography binding = text.gameObject.AddComponent(); + binding.ConfigureForEditor(TypographyRole.Terminal); + addedCount++; + } + } + } + + if (addedCount > 0) + { + EditorSceneManager.MarkSceneDirty(scene); + EditorSceneManager.SaveScene(scene); + } + + return addedCount; + } + finally + { + if (openedForMigration) + EditorSceneManager.CloseScene(scene, true); + } + } + + private static int RemoveRedundantNestedOverrides(TMP_Text text) + { + GameObject sourceObject = PrefabUtility.GetCorrespondingObjectFromSource(text.gameObject); + if (sourceObject == null || sourceObject.GetComponent() == null) + return 0; + + int removed = 0; + LocalizedTypography[] bindings = text.GetComponents(); + foreach (LocalizedTypography binding in bindings) + { + if (!PrefabUtility.IsAddedComponentOverride(binding)) + continue; + + UnityEngine.Object.DestroyImmediate(binding, true); + removed++; + } + + return removed; + } + + private static void ConfigureIfOwnedByCurrentPrefab(LocalizedTypography binding) + { + if (PrefabUtility.IsPartOfPrefabInstance(binding) && + PrefabUtility.GetCorrespondingObjectFromSource(binding) != null) + { + return; + } + + binding.ConfigureForEditor(TypographyRole.Terminal); + } + + private readonly struct MigrationResult + { + public MigrationResult(int textCount, int addedCount, int removedOverrideCount) + { + TextCount = textCount; + AddedCount = addedCount; + RemovedOverrideCount = removedOverrideCount; + } + + public int TextCount { get; } + public int AddedCount { get; } + public int RemovedOverrideCount { get; } + } + } +} diff --git a/Assets/Editor/Typography/TerminalTypographyMigration.cs.meta b/Assets/Editor/Typography/TerminalTypographyMigration.cs.meta new file mode 100644 index 000000000..2f7a874e2 --- /dev/null +++ b/Assets/Editor/Typography/TerminalTypographyMigration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72a6797e130a7f643bd42bf648543ec2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Typography/TypographyFirstRoundBuilder.cs b/Assets/Editor/Typography/TypographyFirstRoundBuilder.cs new file mode 100644 index 000000000..cdfe7c785 --- /dev/null +++ b/Assets/Editor/Typography/TypographyFirstRoundBuilder.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using AibisDream.Framework; +using TMPro; +using UnityEditor; +using UnityEditor.AddressableAssets; +using UnityEditor.AddressableAssets.Settings; +using UnityEditor.AddressableAssets.Settings.GroupSchemas; +using UnityEngine; + +namespace AibisDream.EditorTools +{ + public static class TypographyFirstRoundBuilder + { + private const string GroupName = "Shared_Typography"; + private const string ProfileFolder = "Assets/GameContent/Common/Typography/Profiles"; + private const string WenQuanYiPath = "Assets/Font/Assets/WenQuanYi Bitmap Song 14px SDF.asset"; + private const string ChillBitmapPath = "Assets/Font/Assets/ChillBitmap_16px SDF.asset"; + private const string ArkPixelPath = "Assets/Font/TTF/ark-pixel-16px-proportional-latin SDF.asset"; + + [MenuItem("Tools/AIBIS Dream/Typography/Rebuild First Round Assets")] + public static void BuildFirstRoundAssets() + { + EnsureFolder(ProfileFolder); + + TMP_FontAsset wenQuanYi = LoadRequiredFont(WenQuanYiPath); + TMP_FontAsset chillBitmap = LoadRequiredFont(ChillBitmapPath); + TMP_FontAsset arkPixel = LoadRequiredFont(ArkPixelPath); + + AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings; + if (settings == null) + throw new InvalidOperationException("AddressableAssetSettings 不存在。"); + + AddressableAssetGroup group = GetOrCreateGroup(settings); + CreateOrUpdateProfile( + settings, + group, + TypographyProfileKind.ZhHans, + wenQuanYi, + new[] { chillBitmap }); + CreateOrUpdateProfile( + settings, + group, + TypographyProfileKind.Japanese, + chillBitmap, + new[] { wenQuanYi }); + CreateOrUpdateProfile( + settings, + group, + TypographyProfileKind.Latin, + arkPixel, + new[] { chillBitmap }); + CreateOrUpdateProfile( + settings, + group, + TypographyProfileKind.Russian, + arkPixel, + new[] { chillBitmap }); + + TerminalTypographyMigration.MigrateTerminalPrefabs(); + + EditorUtility.SetDirty(settings); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + Debug.Log("[TypographyFirstRoundBuilder] Profile、Addressables 和首轮试点绑定已更新。"); + } + + public static void BuildFromCommandLine() + { + try + { + BuildFirstRoundAssets(); + } + catch (Exception exception) + { + Debug.LogException(exception); + throw; + } + } + + private static TMP_FontAsset LoadRequiredFont(string path) + { + TMP_FontAsset font = AssetDatabase.LoadAssetAtPath(path); + if (font == null) + throw new InvalidOperationException($"找不到字体资产:{path}"); + return font; + } + + private static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings) + { + AddressableAssetGroup group = settings.FindGroup(GroupName); + if (group == null) + { + group = settings.CreateGroup( + GroupName, + false, + false, + true, + null, + typeof(BundledAssetGroupSchema), + typeof(ContentUpdateGroupSchema)); + } + + BundledAssetGroupSchema bundled = group.GetSchema(); + if (bundled != null) + { + bundled.IncludeInBuild = true; + bundled.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether; + EditorUtility.SetDirty(bundled); + } + + ContentUpdateGroupSchema update = group.GetSchema(); + if (update != null) + { + update.StaticContent = false; + EditorUtility.SetDirty(update); + } + + return group; + } + + private static void CreateOrUpdateProfile( + AddressableAssetSettings settings, + AddressableAssetGroup group, + TypographyProfileKind kind, + TMP_FontAsset font, + IReadOnlyCollection fallbacks) + { + string path = $"{ProfileFolder}/{kind}.asset"; + TypographyProfile profile = AssetDatabase.LoadAssetAtPath(path); + if (profile == null) + { + profile = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(profile, path); + } + + var styles = new List(); + foreach (TypographyRole role in Enum.GetValues(typeof(TypographyRole))) + { + styles.Add(new TypographyStyle( + role, + font, + fallbackFonts: fallbacks, + fontSizeMultiplier: 1f)); + } + + profile.Configure(kind, styles); + EditorUtility.SetDirty(profile); + + string guid = AssetDatabase.AssetPathToGUID(path); + AddressableAssetEntry entry = settings.CreateOrMoveEntry(guid, group, false, false); + entry.address = $"Typography/Profile/{kind}"; + entry.SetLabel("Typography", true, true, false); + } + + private static void EnsureFolder(string path) + { + string[] segments = path.Split('/'); + string current = segments[0]; + for (int i = 1; i < segments.Length; i++) + { + string next = $"{current}/{segments[i]}"; + if (!AssetDatabase.IsValidFolder(next)) + AssetDatabase.CreateFolder(current, segments[i]); + current = next; + } + } + } +} diff --git a/Assets/Editor/Typography/TypographyFirstRoundBuilder.cs.meta b/Assets/Editor/Typography/TypographyFirstRoundBuilder.cs.meta new file mode 100644 index 000000000..6b875305c --- /dev/null +++ b/Assets/Editor/Typography/TypographyFirstRoundBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a84a91ac052fc3444aa281c7d6a4f29e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameContent/Common/Typography.meta b/Assets/GameContent/Common/Typography.meta new file mode 100644 index 000000000..03794cdc1 --- /dev/null +++ b/Assets/GameContent/Common/Typography.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 07d6e97bcfca9a1438f18cb05bb5d75a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameContent/Common/Typography/Profiles.meta b/Assets/GameContent/Common/Typography/Profiles.meta new file mode 100644 index 000000000..282e42f5f --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b6b0068deeac87d4b9557c7d5d38a29d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameContent/Common/Typography/Profiles/Japanese.asset b/Assets/GameContent/Common/Typography/Profiles/Japanese.asset new file mode 100644 index 000000000..c440404bb --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles/Japanese.asset @@ -0,0 +1,53 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5d25e769249077945915a5c2c793b0e6, type: 3} + m_Name: Japanese + m_EditorClassIdentifier: + profileKind: 1 + styles: + - role: 0 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: [] + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 1 + font: {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: [] + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 2 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: [] + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 3 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: f242fb3dde1933640859f1c54591c9c8, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 4 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: f242fb3dde1933640859f1c54591c9c8, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 diff --git a/Assets/GameContent/Common/Typography/Profiles/Japanese.asset.meta b/Assets/GameContent/Common/Typography/Profiles/Japanese.asset.meta new file mode 100644 index 000000000..0db0e3484 --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles/Japanese.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 67cd590df91846b4d81698d68426e616 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameContent/Common/Typography/Profiles/Latin.asset b/Assets/GameContent/Common/Typography/Profiles/Latin.asset new file mode 100644 index 000000000..23bd703d8 --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles/Latin.asset @@ -0,0 +1,56 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5d25e769249077945915a5c2c793b0e6, type: 3} + m_Name: Latin + m_EditorClassIdentifier: + profileKind: 2 + styles: + - role: 0 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 1 + font: {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 2 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 3 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 4 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 diff --git a/Assets/GameContent/Common/Typography/Profiles/Latin.asset.meta b/Assets/GameContent/Common/Typography/Profiles/Latin.asset.meta new file mode 100644 index 000000000..c4bb5b67e --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles/Latin.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d53382c947b5edc4a88216c5fcf37b38 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameContent/Common/Typography/Profiles/Russian.asset b/Assets/GameContent/Common/Typography/Profiles/Russian.asset new file mode 100644 index 000000000..4f79d7c1d --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles/Russian.asset @@ -0,0 +1,54 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5d25e769249077945915a5c2c793b0e6, type: 3} + m_Name: Russian + m_EditorClassIdentifier: + profileKind: 3 + styles: + - role: 0 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: [] + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 1 + font: {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: [] + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 2 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 3 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 4 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 diff --git a/Assets/GameContent/Common/Typography/Profiles/Russian.asset.meta b/Assets/GameContent/Common/Typography/Profiles/Russian.asset.meta new file mode 100644 index 000000000..f11b9c3b8 --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles/Russian.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f82150d4b378dc441884eb47d4f67b7c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameContent/Common/Typography/Profiles/ZhHans.asset b/Assets/GameContent/Common/Typography/Profiles/ZhHans.asset new file mode 100644 index 000000000..3cddc016d --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles/ZhHans.asset @@ -0,0 +1,56 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5d25e769249077945915a5c2c793b0e6, type: 3} + m_Name: ZhHans + m_EditorClassIdentifier: + profileKind: 0 + styles: + - role: 0 + font: {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 1 + font: {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 2 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 3 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 + - role: 4 + font: {fileID: 11400000, guid: 1625bd86098eb0843b1a69eb1b3354e4, type: 2} + materialPreset: {fileID: 0} + fallbackFonts: + - {fileID: 11400000, guid: c7fc4cbba4b38a04c8985d4dbce12a73, type: 2} + fontSizeMultiplier: 1 + lineSpacingOffset: 0 + characterSpacingOffset: 0 diff --git a/Assets/GameContent/Common/Typography/Profiles/ZhHans.asset.meta b/Assets/GameContent/Common/Typography/Profiles/ZhHans.asset.meta new file mode 100644 index 000000000..c12748d59 --- /dev/null +++ b/Assets/GameContent/Common/Typography/Profiles/ZhHans.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e41dce1e1d6e0564b95054b5c8b8bb4c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs b/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs index 9d374dc24..51a901bf0 100644 --- a/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs +++ b/Assets/Scripts/Framework/OtherKit/LocalizationKit.cs @@ -1,5 +1,6 @@ using System.Collections; using System; +using System.Threading; using System.Threading.Tasks; using AibisDream.Utility; using UnityEngine; @@ -11,22 +12,14 @@ using UnityEditor; namespace AibisDream.Framework { - /// - /// csv 本地化列类别。集中声明"哪些 Locale 归到哪一列",csv 数据类按此枚举取字段。 - /// - public enum LocaleKind - { - Cn, - En, - Ja, - Ru, - Es, - PtBr - } - public static class LocalizationKit { private const string LocalizationPrefix = "l10n."; + private static Task currentLanguageSwitchTask; + private static int languageSwitchVersion; + private static bool managedLocaleChange; + private static bool typographyLoaderConfigured; + private static bool localeChangedSubscribed; public static string CurrentLanguageCode { get; private set; } = "zh-Hans"; public static Locale CurrentLocale => LocalizationSettings.SelectedLocale; @@ -78,8 +71,14 @@ namespace AibisDream.Framework { #if UNITY_EDITOR EditorApplication.playModeStateChanged += OnPlayModeChanged; + if (EditorApplication.isPlayingOrWillChangePlaymode) + { + EnsureTypographyLoaderConfigured(); + SubscribeLocaleChanged(); + RefreshCurrentLanguageCodeAsync(); + } #else - LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged; + SubscribeLocaleChanged(); RefreshCurrentLanguageCodeAsync(); #endif } @@ -88,6 +87,14 @@ namespace AibisDream.Framework { UpdateCurrentLanguageCode(locale); EnumEventSystem.Global.Send(SettingChangeEvent.LocaleChanged, locale); + + if (!managedLocaleChange && locale != null) + { + EnsureTypographyLoaderConfigured(); + LocaleKind localeKind = GetLocaleKind(locale.Identifier.Code); + if (!TypographyService.IsActiveFor(localeKind)) + currentLanguageSwitchTask = TypographyService.LoadAndActivateAsync(localeKind); + } } private static void UpdateCurrentLanguageCode(Locale locale) @@ -109,32 +116,95 @@ namespace AibisDream.Framework { if (playMode == PlayModeStateChange.EnteredPlayMode) { - LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged; + EnsureTypographyLoaderConfigured(); + SubscribeLocaleChanged(); RefreshCurrentLanguageCodeAsync(); } else if (playMode == PlayModeStateChange.ExitingPlayMode) { - LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged; + UnsubscribeLocaleChanged(); + TypographyService.ResetForTests(); + typographyLoaderConfigured = false; + currentLanguageSwitchTask = null; } } #endif - public static async void SwitchLanguage(string localeCode) + public static void SwitchLanguage(string localeCode) + { + _ = SwitchLanguageAsync(localeCode); + } + + public static Task SwitchLanguageAsync(string localeCode) + { + int requestVersion = Interlocked.Increment(ref languageSwitchVersion); + currentLanguageSwitchTask = SwitchLanguageCoreAsync(localeCode, requestVersion); + return currentLanguageSwitchTask; + } + + private static async Task SwitchLanguageCoreAsync(string localeCode, int requestVersion) { - // 等待本地化系统初始化完成 await LocalizationSettings.InitializationOperation.Task; var selectedLocale = LocalizationSettings.AvailableLocales.Locales.Find(locale => locale.Identifier.Code == localeCode); + if (!selectedLocale) + { + Debug.LogWarning($"Locale with code {localeCode} not found."); + return false; + } - if (selectedLocale) + EnsureTypographyLoaderConfigured(); + bool typographyReady = await TypographyService.LoadAndActivateAsync(GetLocaleKind(localeCode)); + if (requestVersion != languageSwitchVersion) + return false; + + managedLocaleChange = true; + try { LocalizationSettings.SelectedLocale = selectedLocale; } - else + finally { - Debug.LogWarning($"Locale with code {localeCode} not found."); + managedLocaleChange = false; } + + await Task.Yield(); + Canvas.ForceUpdateCanvases(); + if (!typographyReady) + { + Debug.LogWarning( + $"[LocalizationKit] Locale 已切换为 {localeCode},但字体 Profile 加载失败,保留上一套字体。"); + } + + return true; + } + + private static void EnsureTypographyLoaderConfigured() + { + if (typographyLoaderConfigured) + return; + + TypographyService.ConfigureLoader(new ResourceSystemTypographyProfileLoader()); + typographyLoaderConfigured = true; + } + + private static void SubscribeLocaleChanged() + { + if (localeChangedSubscribed) + return; + + LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged; + localeChangedSubscribed = true; + } + + private static void UnsubscribeLocaleChanged() + { + if (!localeChangedSubscribed) + return; + + LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged; + localeChangedSubscribed = false; } /// @@ -250,6 +320,24 @@ namespace AibisDream.Framework public static IEnumerator WaitUntilReady() { yield return LocalizationSettings.InitializationOperation; + + Task languageTask; + do + { + languageTask = currentLanguageSwitchTask; + while (languageTask != null && !languageTask.IsCompleted) + yield return null; + } while (languageTask != currentLanguageSwitchTask); + + EnsureTypographyLoaderConfigured(); + if (!TypographyService.IsReady) + { + Task typographyTask = TypographyService.LoadAndActivateAsync( + GetLocaleKind(LocalizationSettings.SelectedLocale?.Identifier.Code)); + while (!typographyTask.IsCompleted) + yield return null; + } + yield return null; } diff --git a/Assets/Scripts/Framework/OtherKit/ResourceSystemTypographyProfileLoader.cs b/Assets/Scripts/Framework/OtherKit/ResourceSystemTypographyProfileLoader.cs new file mode 100644 index 000000000..54ad29bb6 --- /dev/null +++ b/Assets/Scripts/Framework/OtherKit/ResourceSystemTypographyProfileLoader.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using UnityEngine.ResourceManagement.AsyncOperations; + +namespace AibisDream.Framework +{ + internal sealed class ResourceSystemTypographyProfileLoader : ITypographyProfileLoader + { + public async Task LoadAsync(string key) + { + AsyncOperationHandle handle = + ResourceSystem.LoadPersistentAsync(key); + try + { + await handle.Task; + if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null) + return handle.Result; + + ResourceSystem.EarlyReleasePersistent(key); + return null; + } + catch + { + ResourceSystem.EarlyReleasePersistent(key); + throw; + } + } + + public void Release(string key) + { + ResourceSystem.EarlyReleasePersistent(key); + } + } +} diff --git a/Assets/Scripts/Framework/OtherKit/ResourceSystemTypographyProfileLoader.cs.meta b/Assets/Scripts/Framework/OtherKit/ResourceSystemTypographyProfileLoader.cs.meta new file mode 100644 index 000000000..a737fb1ec --- /dev/null +++ b/Assets/Scripts/Framework/OtherKit/ResourceSystemTypographyProfileLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c9dd51810ef0cc94398175f3597a978e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Framework/ResourceKit/ResourceSystem.cs b/Assets/Scripts/Framework/ResourceKit/ResourceSystem.cs index dbd3a22ee..234acf0c3 100644 --- a/Assets/Scripts/Framework/ResourceKit/ResourceSystem.cs +++ b/Assets/Scripts/Framework/ResourceKit/ResourceSystem.cs @@ -140,6 +140,15 @@ namespace AibisDream.Framework public static void EarlyRelease(AsyncOperationHandle handle) => ActiveLoader.EarlyRelease(handle); + /// + /// 提前释放持久化 Loader 中指定 Key 的资源。 + /// + public static void EarlyReleasePersistent(string key) + { + if (_persistentLoader != null) + _persistentLoader.EarlyRelease(key); + } + #endregion } } diff --git a/Assets/Scripts/Typography.meta b/Assets/Scripts/Typography.meta new file mode 100644 index 000000000..965a2d3ca --- /dev/null +++ b/Assets/Scripts/Typography.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2964c7221d2d9d44aaa0da470729509b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Typography/AibisDream.Typography.Runtime.asmdef b/Assets/Scripts/Typography/AibisDream.Typography.Runtime.asmdef new file mode 100644 index 000000000..8911403ae --- /dev/null +++ b/Assets/Scripts/Typography/AibisDream.Typography.Runtime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "AibisDream.Typography.Runtime", + "rootNamespace": "AibisDream.Framework", + "references": [ + "Unity.TextMeshPro" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Scripts/Typography/AibisDream.Typography.Runtime.asmdef.meta b/Assets/Scripts/Typography/AibisDream.Typography.Runtime.asmdef.meta new file mode 100644 index 000000000..ff1ccc745 --- /dev/null +++ b/Assets/Scripts/Typography/AibisDream.Typography.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 118b0de59b1eb6e438aec021b8e3f39b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Typography/ITypographyProfileLoader.cs b/Assets/Scripts/Typography/ITypographyProfileLoader.cs new file mode 100644 index 000000000..d321d73b5 --- /dev/null +++ b/Assets/Scripts/Typography/ITypographyProfileLoader.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace AibisDream.Framework +{ + public interface ITypographyProfileLoader + { + Task LoadAsync(string key); + void Release(string key); + } +} diff --git a/Assets/Scripts/Typography/ITypographyProfileLoader.cs.meta b/Assets/Scripts/Typography/ITypographyProfileLoader.cs.meta new file mode 100644 index 000000000..9e5417fc1 --- /dev/null +++ b/Assets/Scripts/Typography/ITypographyProfileLoader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0de13fe1f11da524abe267418428556b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Typography/LocaleKind.cs b/Assets/Scripts/Typography/LocaleKind.cs new file mode 100644 index 000000000..1cf86e16a --- /dev/null +++ b/Assets/Scripts/Typography/LocaleKind.cs @@ -0,0 +1,40 @@ +using System; + +namespace AibisDream.Framework +{ + /// + /// 项目支持的本地化语言类别。多个 Locale 可以共享同一套排版资源。 + /// + public enum LocaleKind + { + Cn, + En, + Ja, + Ru, + Es, + PtBr + } + + public enum TypographyProfileKind + { + ZhHans, + Japanese, + Latin, + Russian + } + + public static class TypographyLocaleMapping + { + public static TypographyProfileKind GetProfileKind(LocaleKind localeKind) => localeKind switch + { + LocaleKind.Cn => TypographyProfileKind.ZhHans, + LocaleKind.Ja => TypographyProfileKind.Japanese, + LocaleKind.Ru => TypographyProfileKind.Russian, + LocaleKind.En or LocaleKind.Es or LocaleKind.PtBr => TypographyProfileKind.Latin, + _ => throw new ArgumentOutOfRangeException(nameof(localeKind), localeKind, null) + }; + + public static string GetProfileKey(LocaleKind localeKind) => + $"Typography/Profile/{GetProfileKind(localeKind)}"; + } +} diff --git a/Assets/Scripts/Typography/LocaleKind.cs.meta b/Assets/Scripts/Typography/LocaleKind.cs.meta new file mode 100644 index 000000000..7c0eac3d4 --- /dev/null +++ b/Assets/Scripts/Typography/LocaleKind.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 65c00ba464255484193f0350427b5e11 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Typography/LocalizedTypography.cs b/Assets/Scripts/Typography/LocalizedTypography.cs new file mode 100644 index 000000000..a2e4534c1 --- /dev/null +++ b/Assets/Scripts/Typography/LocalizedTypography.cs @@ -0,0 +1,89 @@ +using TMPro; +using UnityEngine; + +namespace AibisDream.Framework +{ + [DisallowMultipleComponent] + [RequireComponent(typeof(TMP_Text))] + public sealed class LocalizedTypography : MonoBehaviour + { + [SerializeField] private TypographyRole role = TypographyRole.Body; + [SerializeField] private bool applyMetrics = true; + [SerializeField, HideInInspector] private bool baselineCaptured; + [SerializeField, HideInInspector] private float baseFontSize; + [SerializeField, HideInInspector] private float baseLineSpacing; + [SerializeField, HideInInspector] private float baseCharacterSpacing; + + private TMP_Text target; + + public TypographyRole Role => role; + public TMP_Text Target => target != null ? target : target = GetComponent(); + + private void Reset() + { + target = GetComponent(); + CaptureBaseline(); + } + + private void Awake() + { + target = GetComponent(); + // Prefab variants can override TMP metrics without overriding the + // baseline serialized by the source prefab. Capture the fully + // resolved instance values before the first profile is applied. + CaptureBaseline(); + } + + private void OnEnable() + { + if (!baselineCaptured) + CaptureBaseline(); + TypographyService.Register(this); + } + + private void OnDisable() + { + TypographyService.Unregister(this); + } + + public void ApplyProfile(TypographyProfile profile) + { + TMP_Text text = Target; + TypographyStyle style = profile != null ? profile.Resolve(role) : null; + if (text == null || style == null || !style.IsUsable) + return; + + if (!baselineCaptured) + CaptureBaseline(); + TypographyService.ApplyStyle( + text, + style, + applyMetrics, + baseFontSize, + baseLineSpacing, + baseCharacterSpacing); + } + + public void CaptureBaseline() + { + TMP_Text text = Target; + if (text == null) + return; + + baseFontSize = text.fontSize; + baseLineSpacing = text.lineSpacing; + baseCharacterSpacing = text.characterSpacing; + baselineCaptured = true; + } + +#if UNITY_EDITOR + public void ConfigureForEditor(TypographyRole configuredRole, bool shouldApplyMetrics = true) + { + role = configuredRole; + applyMetrics = shouldApplyMetrics; + CaptureBaseline(); + UnityEditor.EditorUtility.SetDirty(this); + } +#endif + } +} diff --git a/Assets/Scripts/Typography/LocalizedTypography.cs.meta b/Assets/Scripts/Typography/LocalizedTypography.cs.meta new file mode 100644 index 000000000..9ccd00038 --- /dev/null +++ b/Assets/Scripts/Typography/LocalizedTypography.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9fe642db0d3d2b54fa6d1afaf8c85e4a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Typography/TypographyProfile.cs b/Assets/Scripts/Typography/TypographyProfile.cs new file mode 100644 index 000000000..77ce1507c --- /dev/null +++ b/Assets/Scripts/Typography/TypographyProfile.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using TMPro; +using UnityEngine; + +namespace AibisDream.Framework +{ + [CreateAssetMenu(fileName = "TypographyProfile", menuName = "AIBIS Dream/Typography/Profile")] + public sealed class TypographyProfile : ScriptableObject + { + [SerializeField] private TypographyProfileKind profileKind; + [SerializeField] private List styles = new(); + + public TypographyProfileKind ProfileKind => profileKind; + public IReadOnlyList Styles => styles; + + public TypographyStyle Resolve(TypographyRole role) + { + TypographyStyle body = null; + if (styles == null) + return null; + + foreach (TypographyStyle style in styles) + { + if (style == null || !style.IsUsable) + continue; + + if (style.Role == role) + return style; + if (style.Role == TypographyRole.Body) + body = style; + } + + return body; + } + + public IEnumerable GetValidationErrors() + { + if (styles == null || styles.Count == 0) + { + yield return $"{name}: 没有配置任何 TypographyStyle。"; + yield break; + } + + var seenRoles = new HashSet(); + foreach (TypographyStyle style in styles) + { + if (style == null) + { + yield return $"{name}: 存在空的 TypographyStyle。"; + continue; + } + + if (!seenRoles.Add(style.Role)) + yield return $"{name}: Role {style.Role} 重复配置。"; + if (!style.IsUsable) + yield return $"{name}: Role {style.Role} 没有字体。"; + } + + if (!seenRoles.Contains(TypographyRole.Body)) + yield return $"{name}: 缺少 Body 回退样式。"; + } + + public void Configure(TypographyProfileKind kind, IEnumerable configuredStyles) + { + profileKind = kind; + styles = configuredStyles != null + ? new List(configuredStyles) + : new List(); + } + + internal void ApplyFallbacks() + { + if (styles == null) + return; + + var configuredFonts = new HashSet(); + foreach (TypographyStyle style in styles) + { + if (style?.Font != null && configuredFonts.Add(style.Font)) + style.ApplyFallbacks(); + } + } + } +} diff --git a/Assets/Scripts/Typography/TypographyProfile.cs.meta b/Assets/Scripts/Typography/TypographyProfile.cs.meta new file mode 100644 index 000000000..5bb44138c --- /dev/null +++ b/Assets/Scripts/Typography/TypographyProfile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d25e769249077945915a5c2c793b0e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Typography/TypographyRole.cs b/Assets/Scripts/Typography/TypographyRole.cs new file mode 100644 index 000000000..e513851e8 --- /dev/null +++ b/Assets/Scripts/Typography/TypographyRole.cs @@ -0,0 +1,11 @@ +namespace AibisDream.Framework +{ + public enum TypographyRole + { + Body, + Dialogue, + Terminal, + WorldText, + MiniGame + } +} diff --git a/Assets/Scripts/Typography/TypographyRole.cs.meta b/Assets/Scripts/Typography/TypographyRole.cs.meta new file mode 100644 index 000000000..0373cfd70 --- /dev/null +++ b/Assets/Scripts/Typography/TypographyRole.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e847b9aaee116be4c8d8d135ae0defb9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Typography/TypographyService.cs b/Assets/Scripts/Typography/TypographyService.cs new file mode 100644 index 000000000..459c56c2f --- /dev/null +++ b/Assets/Scripts/Typography/TypographyService.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TMPro; +using UnityEngine; + +namespace AibisDream.Framework +{ + public static class TypographyService + { + private static readonly HashSet Bindings = new(); + private static ITypographyProfileLoader loader; + private static int switchVersion; + private static string currentProfileKey; + private static string latestRequestedProfileKey; + private static TypographyProfile currentProfile; + + public static TypographyProfile CurrentProfile => currentProfile; + public static bool IsReady => currentProfile != null; + public static string CurrentProfileKey => currentProfileKey; + + public static void ConfigureLoader(ITypographyProfileLoader profileLoader) + { + loader = profileLoader ?? throw new ArgumentNullException(nameof(profileLoader)); + } + + public static bool IsActiveFor(LocaleKind localeKind) => + currentProfile != null && + string.Equals( + currentProfileKey, + TypographyLocaleMapping.GetProfileKey(localeKind), + StringComparison.Ordinal); + + public static TypographyStyle Resolve(TypographyRole role) => currentProfile?.Resolve(role); + + public static bool Apply(TMP_Text text, TypographyRole role, bool applyMetrics = true) + { + if (text == null) + return false; + + TypographyStyle style = Resolve(role); + if (style == null || !style.IsUsable) + return false; + + ApplyStyle(text, style, applyMetrics, text.fontSize, text.lineSpacing, text.characterSpacing); + return true; + } + + public static void Register(LocalizedTypography binding) + { + if (binding == null) + return; + + Bindings.Add(binding); + if (currentProfile != null) + binding.ApplyProfile(currentProfile); + } + + public static void Unregister(LocalizedTypography binding) + { + if (binding != null) + Bindings.Remove(binding); + } + + public static async Task LoadAndActivateAsync(LocaleKind localeKind) + { + int requestVersion = ++switchVersion; + string profileKey = TypographyLocaleMapping.GetProfileKey(localeKind); + latestRequestedProfileKey = profileKey; + + if (IsActiveFor(localeKind)) + return true; + if (loader == null) + { + Debug.LogError("[TypographyService] 尚未配置 ITypographyProfileLoader。"); + return false; + } + + TypographyProfile loadedProfile; + try + { + loadedProfile = await loader.LoadAsync(profileKey); + } + catch (Exception exception) + { + Debug.LogError($"[TypographyService] 加载字体 Profile 失败:{profileKey}\n{exception}"); + return false; + } + + if (requestVersion != switchVersion) + { + if (!string.Equals(profileKey, currentProfileKey, StringComparison.Ordinal) && + !string.Equals(profileKey, latestRequestedProfileKey, StringComparison.Ordinal)) + { + loader.Release(profileKey); + } + return false; + } + if (loadedProfile == null) + { + Debug.LogError($"[TypographyService] 字体 Profile 不存在或加载失败:{profileKey}"); + return false; + } + + string previousKey = currentProfileKey; + loadedProfile.ApplyFallbacks(); + currentProfile = loadedProfile; + currentProfileKey = profileKey; + RefreshBindings(); + Canvas.ForceUpdateCanvases(); + + await Task.Yield(); + if (!string.IsNullOrEmpty(previousKey) && + !string.Equals(previousKey, profileKey, StringComparison.Ordinal)) + { + loader.Release(previousKey); + } + + return true; + } + + internal static void ApplyStyle( + TMP_Text text, + TypographyStyle style, + bool applyMetrics, + float baseFontSize, + float baseLineSpacing, + float baseCharacterSpacing) + { + text.font = style.Font; + text.fontSharedMaterial = style.MaterialPreset != null + ? style.MaterialPreset + : style.Font.material; + + if (applyMetrics) + { + text.fontSize = baseFontSize * style.FontSizeMultiplier; + text.lineSpacing = baseLineSpacing + style.LineSpacingOffset; + text.characterSpacing = baseCharacterSpacing + style.CharacterSpacingOffset; + } + + text.SetAllDirty(); + if (text.gameObject.activeInHierarchy) + text.ForceMeshUpdate(); + } + + private static void RefreshBindings() + { + if (Bindings.Count == 0) + return; + + var stale = new List(); + foreach (LocalizedTypography binding in Bindings) + { + if (binding == null) + stale.Add(binding); + else + binding.ApplyProfile(currentProfile); + } + + foreach (LocalizedTypography binding in stale) + Bindings.Remove(binding); + } + +#if UNITY_EDITOR + public static void ResetForTests() + { + if (loader != null && !string.IsNullOrEmpty(currentProfileKey)) + loader.Release(currentProfileKey); + loader = null; + currentProfile = null; + currentProfileKey = null; + latestRequestedProfileKey = null; + switchVersion++; + Bindings.Clear(); + } +#endif + } +} diff --git a/Assets/Scripts/Typography/TypographyService.cs.meta b/Assets/Scripts/Typography/TypographyService.cs.meta new file mode 100644 index 000000000..70f090e3e --- /dev/null +++ b/Assets/Scripts/Typography/TypographyService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c15158900e46184da7832703b02d0a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Typography/TypographyStyle.cs b/Assets/Scripts/Typography/TypographyStyle.cs new file mode 100644 index 000000000..df6a38df2 --- /dev/null +++ b/Assets/Scripts/Typography/TypographyStyle.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using TMPro; +using UnityEngine; + +namespace AibisDream.Framework +{ + [Serializable] + public sealed class TypographyStyle + { + [SerializeField] private TypographyRole role; + [SerializeField] private TMP_FontAsset font; + [SerializeField] private Material materialPreset; + [SerializeField] private List fallbackFonts = new(); + [SerializeField, Min(0.01f)] private float fontSizeMultiplier = 1f; + [SerializeField] private float lineSpacingOffset; + [SerializeField] private float characterSpacingOffset; + + public TypographyStyle( + TypographyRole role, + TMP_FontAsset font, + Material materialPreset = null, + IEnumerable fallbackFonts = null, + float fontSizeMultiplier = 1f, + float lineSpacingOffset = 0f, + float characterSpacingOffset = 0f) + { + this.role = role; + this.font = font; + this.materialPreset = materialPreset; + this.fallbackFonts = fallbackFonts != null + ? new List(fallbackFonts) + : new List(); + this.fontSizeMultiplier = Mathf.Max(0.01f, fontSizeMultiplier); + this.lineSpacingOffset = lineSpacingOffset; + this.characterSpacingOffset = characterSpacingOffset; + } + + public TypographyRole Role => role; + public TMP_FontAsset Font => font; + public Material MaterialPreset => materialPreset; + public IReadOnlyList FallbackFonts => fallbackFonts; + public float FontSizeMultiplier => Mathf.Max(0.01f, fontSizeMultiplier); + public float LineSpacingOffset => lineSpacingOffset; + public float CharacterSpacingOffset => characterSpacingOffset; + public bool IsUsable => font != null; + + internal void ApplyFallbacks() + { + if (font == null) + return; + + font.fallbackFontAssetTable ??= new List(); + font.fallbackFontAssetTable.Clear(); + if (fallbackFonts == null) + return; + + foreach (TMP_FontAsset fallback in fallbackFonts) + { + if (fallback != null && fallback != font && !font.fallbackFontAssetTable.Contains(fallback)) + font.fallbackFontAssetTable.Add(fallback); + } + } + } +} diff --git a/Assets/Scripts/Typography/TypographyStyle.cs.meta b/Assets/Scripts/Typography/TypographyStyle.cs.meta new file mode 100644 index 000000000..32dfdb233 --- /dev/null +++ b/Assets/Scripts/Typography/TypographyStyle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50206284a91c06448b41453dbf51dc44 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UI/Terminal/TerminalChapterPanel.cs b/Assets/Scripts/UI/Terminal/TerminalChapterPanel.cs index acd993266..06db9e003 100644 --- a/Assets/Scripts/UI/Terminal/TerminalChapterPanel.cs +++ b/Assets/Scripts/UI/Terminal/TerminalChapterPanel.cs @@ -83,7 +83,7 @@ namespace AibisDream.UI private void PrepareContent() { - _owner?.SetHeader("terminal_header_chapter", "REPAIR_STATION_07 - AUTH_VERIFIED"); + _owner?.SetHeader("terminal_header_chapter", "AUTH_VERIFIED"); LoadChapters(); RefreshViews(); } diff --git a/Assets/Scripts/UI/Terminal/TerminalRecordPanel.cs b/Assets/Scripts/UI/Terminal/TerminalRecordPanel.cs index e928a322c..c043af130 100644 --- a/Assets/Scripts/UI/Terminal/TerminalRecordPanel.cs +++ b/Assets/Scripts/UI/Terminal/TerminalRecordPanel.cs @@ -74,7 +74,7 @@ namespace AibisDream.UI public void Show() { - _owner?.SetHeader("termnial_header_record", "REPAIR_STATION_07 - AUTH_VERIFIED"); + _owner?.SetHeader("termnial_header_record", "AUTH_VERIFIED"); if (panelAnimator != null) { panelAnimator.Show(onComplete: () => StartCoroutine(DrawRecordsWhenReady())); diff --git a/Assets/Scripts/UI/Terminal/TerminalSettingPanel.cs b/Assets/Scripts/UI/Terminal/TerminalSettingPanel.cs index 2efa5a7a7..b3a6c2a8b 100644 --- a/Assets/Scripts/UI/Terminal/TerminalSettingPanel.cs +++ b/Assets/Scripts/UI/Terminal/TerminalSettingPanel.cs @@ -38,7 +38,7 @@ namespace AibisDream.UI new UIFormOption { prop = "ja-JP", label = "日本語" }, new UIFormOption { prop = "ru", label = "Русский" }, new UIFormOption { prop = "es", label = "Español" }, - new UIFormOption { prop = "pt-BR", label = "Português (Brasil)" }, + new UIFormOption { prop = "pt-BR", label = "Português" }, }; private static readonly UIFormOption[] TextSpeedOptions = @@ -115,7 +115,7 @@ namespace AibisDream.UI private void PrepareContent() { - _owner?.SetHeader("terminal_header_setting", "REPAIR_STATION_07 - AUTH_VERIFIED"); + _owner?.SetHeader("terminal_header_setting", "AUTH_VERIFIED"); InitControls(); RefreshControls(); RefreshFooterButtons(); diff --git a/Assets/Scripts/UI/Terminal/TerminalStartPanel.cs b/Assets/Scripts/UI/Terminal/TerminalStartPanel.cs index a75160948..2b681a1d2 100644 --- a/Assets/Scripts/UI/Terminal/TerminalStartPanel.cs +++ b/Assets/Scripts/UI/Terminal/TerminalStartPanel.cs @@ -49,11 +49,11 @@ namespace AibisDream.UI if (panelAnimator != null) { - panelAnimator.Show(prepare: () => _owner?.SetHeader("termnial_header_start", "REPAIR_STATION_07 - AUTH_VERIFIED")); + panelAnimator.Show(prepare: () => _owner?.SetHeader("termnial_header_start", "AUTH_VERIFIED")); } else { - _owner?.SetHeader("termnial_header_start", "REPAIR_STATION_07 - AUTH_VERIFIED"); + _owner?.SetHeader("termnial_header_start", "AUTH_VERIFIED"); gameObject.SetActive(true); } } diff --git a/Assets/StreamingAssets/Config/character.csv b/Assets/StreamingAssets/Config/character.csv index 488abe96a..22533d80a 100644 --- a/Assets/StreamingAssets/Config/character.csv +++ b/Assets/StreamingAssets/Config/character.csv @@ -1,77 +1,76 @@ 通过key索引配置,中文名,英文名,日文名,俄文名,西班牙文名,巴西葡萄牙文名,"显示位置,Default使用默认,Screen插线屏幕,OldBox基本对话框",人物角色(Player/MainActor/Aside),名称显示时的颜色,头像Pic路径,语音资源路径,气泡序号(默认0) Key,Cn,En,Ja,Ru,Es,PtBr,DialogViewType,Role,NameColor,HeadPicPath,VoicePath,BubbleIdx -jg,警告,Warning,Warning,,,,Default,MainActor,#7ae622,,VO/voice_DN, -门,门,Door,,,,,Default,MainActor,#7ae622,,, -矩形,黑暗,Darkness,,,,,Default,MainActor,#7ae622,,VO/voice_DN, -记忆,记忆,Memory,,,,,Default,MainActor,#7ae622,,, -peipei,佩佩,Peipei,ペイペイ,,,,Default,MainActor,#00FFFF,peipei_head,VO/voice_PP, -乌鸦,乌鸦,Crow,,,,,Default,MainActor,#e6ddc1,,VO/voice_WYd, -wyd?,???,???,???,,,,Default,MainActor,#e6ddc1,,VO/voice_WYd, -马尾辫女孩,马尾辫女孩,Girl with ponytail,,,,,Default,MainActor,,,VO/voice_JJ, -pp,佩佩,Peipei,ペイペイ,,,,Default,MainActor,#00FFFF,peipei_head,VO/voice_PP, -ppm,佩佩,Peipei,ペイペイ,,,,Default,MainActor,#00FFFF,peipei_head,VO/voice_PPm, -ppd,女孩,Girl,,,,,Default,MainActor,,,VO/voice_PPd, -ppd?,女孩,Girl,,,,,Default,MainActor,,,VO/voice_PPd, -me,路斯,Luth,ルース,,,,Default,Player,,me_head,VO/voice_ME, -med,路斯,Luth,ルース,,,,Default,Player,,me_head,VO/voice_MEd, -mem,路斯,Luth,ルース,,,,Default,Player,,me_head,VO/voice_MEm, -mecd,女孩,Young Girl,少女,,,,Default,MainActor,,,VO/voice_MECd, -mecd?,???,???,???,,,,Default,MainActor,,,VO/voice_MECd, -bb,BB机,Beeper,,,,,Default,Player,,me_head,, -jj,姐姐,Sister,姉さん,,,,Default,MainActor,,me_head,VO/voice_JJ, -jjd,姐姐,Sister,姉さん,,,,Default,MainActor,,me_head,VO/voice_JJd, -jjm,姐姐,Sister,姉さん,,,,Default,MainActor,,me_head,VO/voice_JJm, -jj?,柔和女声,???,???,,,,Default,MainActor,,me_head,VO/voice_JJ, -hs,火山,HuoShan,火山,,,,Default,MainActor,#FFA800,peipei_head,VO/voice_HS, -hs?,沙哑男声,???,???,,,,Default,MainActor,#FFA800,peipei_head,VO/voice_HS, -hsm,火山,HuoShan,火山,,,,Default,MainActor,,,VO/voice_HSm, -hsd,阿灿,Young Boy,少年,,,,Default,MainActor,,,VO/voice_HSd, -hsd?,???,???,???,,,,Default,MainActor,#FFA800,peipei_head,VO/voice_HSd, -yl,英理,Yingli,エリ,,,,Default,MainActor,#4000FF,peipei_head,VO/voice_YL, -ql?,02号调酒机体,Bartender,,,,,Default,MainActor,#00FFFF,peipei_head,VO/voice_YL, -ql,奇琳,Kirin,チーリン,,,,Default,MainActor,#00FFFF,peipei_head,VO/voice_YL, -peipei_novoice,胆怯女声,Timid female voice,怯えた女声,,,,Default,Aside,#00FFFF,,VO/voice_PP,2 -yingli_novoice,严厉女声,Stern female voice,厳しい女声,,,,Default,Aside,#FFCC00,,VO/voice_YL,1 -tutorial,教学,Tutorial,,,,,Default,Aside,,,, -docter,儿科医生,Doctor (in memory),医者(記憶の中),,,,Default,Aside,#FFCC00,,, -peipei_child,童年佩佩,Peipei (child),ペイペイ(幼少期),,,,Default,Aside,#00FFFF,,VO/voice_PPm, -unknown,???,???,???,,,,Default,Aside,#FFCC00,,, -???,???,???,???,,,,Default,MainActor,#FFCC00,,, -zls?,???,???,???,,,,Default,MainActor,#FFCC00,,VO/voice_memCommon, -zls,治疗师,Therapist,セラピスト,,,,Default,MainActor,#FFCC00,,VO/voice_memCommon, -shitou,石头,Stone,シートウ,,,,Default,MainActor,,,VO/voice_shitou, -Descent,Descent,Descent,,,,,Default,Aside,#FFCC00,,VO/voice_me, -e-assistant,电子助手,Electronic Assistant,,,,,Default,Aside,#FFCC00,,VO/voice_me, -Gotashi_unknow,戴纯金雕塑头的男人,*Gotash,*ゴタシュ,,,,Default,Aside,#FFCC00,,, -dn,无连接,No Connection,No Connection,,,,Screen,Aside,#FFBF5F,,VO/voice_DN, -Linking,连接中……,Connecting...,Connecting...,,,,Screen,Aside,#E6A23C,,VO/voice_DN, -Analysing,分析中,Analyzing...,Analyzing...,,,,Screen,Aside,#E6A23C,,VO/voice_DN, -Error,错误警告,Error warning,Error warning,,,,Screen,Aside,#E6A23C,,VO/voice_DN, -UfModule,Unstable Fusion图像生成模块,Unstable Fusion Module,Unstable Fusion Module,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -EyeModule,HS9型视觉模块,HS9 Visual Module,HS9 Visual Module,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -EmoModule,通用型情绪发生模块,Universal Emotion Generation Module,Universal Emotion Generation Module,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -MemModule,通用型记忆模块,Universal Memory Module,Universal Memory Module,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -LogicModule,MK1型逻辑处理模块,MK1 logic Processing Module,MK1 logic Processing Module,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -SpeakerModule,MK1型表达模块,MK1 Expression Module,MK1 Expression Module,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -SaleModule,销售语言拓展模块,Sales Language Extension Module,販売言語拡張モジュール,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -ChipModule,4R7型色觉晶圆训练槽,4R7 Color Perception Wafer Training Slot,4R7 Color Perception Wafer Training Slot,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -HivemindModule,未知型号
Hivemind通讯终端,Unknown type Hivemind communication terminal,Unknown type Hivemind communication terminal,,,,Screen,Aside,#F56C6C,,VO/voice_DN, -HeartModule,C03UR型特配涡轮增压动力模块,C03UR Special Turbocharged Power Module,C03UR Special Turbocharged Power Module,,,,Screen,Aside,#E6A23C,,VO/voice_DN, -LungModule,MK1型气体处理模块,MK1 Gas Processing Module,MK1 Gas Processing Module,,,,Screen,Aside,#E6A23C,,VO/voice_DN, -LiverModule,MK1型冷却液循环模块,MK1 Coolant Circulation Module,MK1 Coolant Circulation Module,,,,Screen,Aside,#E6A23C,,VO/voice_DN, -DigestModule,MK1型物料处理模块,MK1 Material Processing Module,MK1 Material Processing Module,,,,Screen,Aside,#E6A23C,,VO/voice_DN, +jg,警告,Warning,警告,Предупреждение,Advertencia,Aviso,Default,MainActor,#7ae622,,VO/voice_DN, +门,门,Door,ドア,Дверь,Puerta,Porta,Default,MainActor,#7ae622,,, +矩形,黑暗,Darkness,闇,Тьма,Oscuridad,Escuridão,Default,MainActor,#7ae622,,VO/voice_DN, +记忆,记忆,Memory,記憶,Память,Memoria,Memória,Default,MainActor,#7ae622,,, +peipei,佩佩,PeiPei,ペペ,Пэйпэй,Peipei,Peipei,Default,MainActor,#00FFFF,peipei_head,VO/voice_PP, +乌鸦,乌鸦,Crow,カラス,Ворон,Cuervo,Corvo,Default,MainActor,#e6ddc1,,VO/voice_WYd, +wyd?,???,???,???,???,???,???,Default,MainActor,#e6ddc1,,VO/voice_WYd, +马尾辫女孩,马尾辫女孩,Ponytail Girl,ポニーテールの女の子,Девочка с хвостиком,Chica de la coleta,Garota de Rabo de Cavalo,Default,MainActor,,,VO/voice_JJ, +pp,佩佩,PeiPei,ペペ,Пэйпэй,Peipei,Peipei,Default,MainActor,#00FFFF,peipei_head,VO/voice_PP, +ppm,佩佩,PeiPei,ペペ,Пэйпэй,Peipei,Peipei,Default,MainActor,#00FFFF,peipei_head,VO/voice_PPm, +ppd,女孩,Girl,女の子,Девочка,Niña,Garota,Default,MainActor,,,VO/voice_PPd, +ppd?,女孩,Girl,女の子,Девочка,Niña,Garota,Default,MainActor,,,VO/voice_PPd, +me,路斯,Ruth,ルース,Лус,Ruth,Lusi,Default,Player,,me_head,VO/voice_ME, +med,路斯,Ruth,ルース,Лус,Ruth,Lusi,Default,Player,,me_head,VO/voice_MEd, +mem,路斯,Ruth,ルース,Лус,Ruth,Lusi,Default,Player,,me_head,VO/voice_MEm, +mecd,女孩,Girl,女の子,Девочка,Niña,Garota,Default,MainActor,,,VO/voice_MECd, +mecd?,???,???,???,???,???,???,Default,MainActor,,,VO/voice_MECd, +bb,BB机,Pager,ポケベル,Пейджер,Bíper,Pager,Default,Player,,me_head,, +jj,姐姐,Sister,姉さん,Сестра,Hermana mayor,Irmã,Default,MainActor,,me_head,VO/voice_JJ, +jjd,姐姐,Sister,姉さん,Сестра,Hermana mayor,Irmã,Default,MainActor,,me_head,VO/voice_JJd, +jjm,姐姐,Sister,姉さん,Сестра,Hermana mayor,Irmã,Default,MainActor,,me_head,VO/voice_JJm, +jj?,柔和女声,Soft Female Voice,柔らかな女の声,Нежный женский голос,Voz suave de mujer,Voz Feminina Suave,Default,MainActor,,me_head,VO/voice_JJ, +hs,火山,Cole,火山,Вулкан,Huoshan,Vulcão,Default,MainActor,#FFA800,peipei_head,VO/voice_HS, +hs?,沙哑男声,Hoarse Male Voice,しゃがれた男の声,Хриплый мужской голос,Voz ronca de hombre,Voz Masculina Rouca,Default,MainActor,#FFA800,peipei_head,VO/voice_HS, +hsm,火山,Kazan,火山,Вулкан,Huoshan,Vulcão,Default,MainActor,,,VO/voice_HSm, +hsd,阿灿,Acan,アーツァン,А-Цань,Acan,Acan,Default,MainActor,,,VO/voice_HSd, +hsd?,???,???,???,???,???,???,Default,MainActor,#FFA800,peipei_head,VO/voice_HSd, +yl,英理,Yingli,エリ,Инь Ли,Yingli,Yingli,Default,MainActor,#4000FF,peipei_head,VO/voice_YL, +ql?,02号调酒机体,Bartender Entity 02,02番バーテンダー機体,Юнит-бармен №02,Unidad barman 02,Unidade Bartender 02,Default,MainActor,#00FFFF,peipei_head,VO/voice_YL, +ql,奇琳,Qilin,チーリン,Цилинь,Qilin,Qilin,Default,MainActor,#00FFFF,peipei_head,VO/voice_YL, +peipei_novoice,胆怯女声,Timid Female Voice,臆病な女の声,Робкий женский голос,Voz tímida de mujer,Voz Feminina Tímida,Default,Aside,#00FFFF,,VO/voice_PP,2 +yingli_novoice,严厉女声,Stern Female Voice,厳しい女の声,Строгий женский голос,Voz severa de mujer,Voz Feminina Severa,Default,Aside,#FFCC00,,VO/voice_YL,1 +tutorial,教学,Tutorial,チュートリアル,Обучение,Tutorial,Tutorial,Default,Aside,,,, +docter,儿科医生,Pediatrician,小児科医,Педиатр,Pediatra,Pediatra,Default,Aside,#FFCC00,,, +peipei_child,童年佩佩,Peipei (child),ペペ(幼少期),Пэйпэй в детстве,Peipei (niña),PeiPei Criança,Default,Aside,#00FFFF,,VO/voice_PPm, +unknown,???,???,???,???,???,???,Default,Aside,#FFCC00,,, +???,???,???,???,???,???,???,Default,MainActor,#FFCC00,,, +zls?,???,???,???,???,???,???,Default,MainActor,#FFCC00,,VO/voice_memCommon, +zls,治疗师,Therapist,セラピスト,Терапевт,Terapeuta,Terapeuta,Default,MainActor,#FFCC00,,VO/voice_memCommon, +shitou,石头,Stone,ストーン,Шитоу,Piedra,Pedra,Default,MainActor,,,VO/voice_shitou, +Descent,Descent,Descent,ディセント,Descent,Descent,Descent,Default,Aside,#FFCC00,,VO/voice_me, +e-assistant,电子助手,Electronic Assistant,電子アシスタント,Электронный помощник,Asistente electrónico,Assistente Eletrônico,Default,Aside,#FFCC00,,VO/voice_me, +Gotashi_unknow,戴纯金雕塑头的男人,Man with a Solid Gold Sculpted Head,純金彫刻の頭部を被った男,Мужчина с головой-скульптурой из чистого золота,Hombre que lleva una cabeza de escultura de oro puro,Homem com Cabeça de Escultura de Ouro Maciço,Default,Aside,#FFCC00,,, +dn,无连接,Disconnected,未接続,Нет подключения,Sin conexión,Desconectado,Screen,Aside,#FFBF5F,,VO/voice_DN, +Linking,连接中……,Connecting...,接続中……,Подключение...,Conectando...,Conectando...,Screen,Aside,#E6A23C,,VO/voice_DN, +Analysing,分析中,Analyzing...,分析中,Анализ...,Analizando,Analisando,Screen,Aside,#E6A23C,,VO/voice_DN, +Error,错误警告,Error Warning,エラー警告,Предупреждение об ошибке,Advertencia de error,Aviso de Erro,Screen,Aside,#E6A23C,,VO/voice_DN, +UfModule,Unstable Fusion图像生成模块,Unstable Fusion Image Generation Module,Unstable Fusion画像生成モジュール,Модуль генерации изображений Unstable Fusion,Módulo de generación de imágenes Unstable Fusion,Módulo de Geração de Imagem Unstable Fusion,Screen,Aside,#F56C6C,,VO/voice_DN, +EyeModule,HS9型视觉模块,HS9 Vision Module,HS9型視覚モジュール,Модуль зрения HS9,Módulo visual modelo HS9,Módulo de Visão HS9,Screen,Aside,#F56C6C,,VO/voice_DN, +EmoModule,通用型情绪发生模块,General-Purpose Emotion Generation Module,汎用型情緒発生モジュール,Универсальный модуль генерации эмоций,Módulo emocional genérico,Módulo de Geração de Emoção Genérico,Screen,Aside,#F56C6C,,VO/voice_DN, +MemModule,通用型记忆模块,Universal Memory Module,汎用型記憶モジュール,Универсальный модуль памяти,Módulo de memoria genérico,Módulo de Memória Genérico,Screen,Aside,#F56C6C,,VO/voice_DN, +LogicModule,MK1型逻辑处理模块,Type MK1 Logic Processing Module,MK1型論理処理モジュール,Модуль обработки логики MK1,Módulo de procesamiento lógico modelo MK1,Módulo de Processamento Lógico MK1,Screen,Aside,#F56C6C,,VO/voice_DN, +SpeakerModule,MK1型表达模块,Type MK1 Expression Module,MK1型表現モジュール,Модуль выражения MK1,Módulo de expresión modelo MK1,Módulo de Expressão MK1,Screen,Aside,#F56C6C,,VO/voice_DN, +SaleModule,销售语言拓展模块,Sales Language Extension Module,販売言語拡張モジュール,Модуль расширения языка продаж,Módulo de expansión de lenguaje de ventas,Módulo de Expansão de Linguagem de Vendas,Screen,Aside,#F56C6C,,VO/voice_DN, +ChipModule,4R7型色觉晶圆训练槽,Type 4R7 Color Vision Wafer Training Slot,4R7型色覚ウェハートレーニングスロット,Тренировочный слот чипа цветовосприятия 4R7,Ranura de entrenamiento de oblea de visión cromática modelo 4R7,Slot de Treinamento do Chip de Visão Cromática 4R7,Screen,Aside,#F56C6C,,VO/voice_DN, +HivemindModule,未知型号
Hivemind通讯终端,Unknown Model
Hivemind Communication Terminal,型番不明
ハイブマインド通信端末,Терминал связи «Коллективный разум»
неизвестной модели,Terminal de comunicación Hivemind
de modelo desconocido,Modelo Desconhecido
Terminal de Comunicação Hivemind,Screen,Aside,#F56C6C,,VO/voice_DN, +HeartModule,C03UR型特配涡轮增压动力模块,Type C03UR Custom Turbocharged Power Module,C03UR型特注ターボチャージャー動力モジュール,Специальный силовой модуль C03UR с турбонаддувом,Módulo de potencia turboalimentado especial modelo C03UR,Módulo de Potência Turbocompressor Personalizado C03UR,Screen,Aside,#E6A23C,,VO/voice_DN, +LungModule,MK1型气体处理模块,Type MK1 Gas Processing Module,MK1型気体処理モジュール,Модуль обработки газов MK1,Módulo de procesamiento de gas modelo MK1,Módulo de Processamento de Gás MK1,Screen,Aside,#E6A23C,,VO/voice_DN, +LiverModule,MK1型冷却液循环模块,Type MK1 Coolant Circulation Module,MK1型冷却液循環モジュール,Модуль циркуляции хладагента MK1,Módulo de circulación de líquido refrigerante modelo MK1,Módulo de Circulação de Líquido de Arrefecimento MK1,Screen,Aside,#E6A23C,,VO/voice_DN, +DigestModule,MK1型物料处理模块,Type MK1 Material Processing Module,MK1型原料処理モジュール,Модуль обработки материалов MK1,Módulo de procesamiento de material modelo MK1,Módulo de Processamento de Material MK1,Screen,Aside,#E6A23C,,VO/voice_DN, CenterText,,,,,,,CenterText,Aside,,,, CenterOption,,,,,,,CenterOption,Aside,,,, Task,,,,,,,Task,Aside,,,, TempText,,,,,,,Default,Aside,,,, -gts,患者,???,???,,,,Default,MainActor,#A4CD36,,VO/voice_GTS, -gts?,患者,???,???,,,,Default,MainActor,#A4CD36,,VO/voice_GTS_broken, -gts??,患者??,???,???,,,,Default,MainActor,#E2C22B,,VO/voice_GTS, -classmate,同学,Classmate,クラスメート,,,,Default,Aside,,,VO/voice_memCommon, -jb,酒保,Bartender,バーテンダー,,,,Default,MainActor,,,VO/voice_memCommon, -host,主持人,Newscaster,ニュースキャスター,,,,Default,MainActor,,,VO/voice_memCommon, -teacher,教师,Teacher,Teacher,,,,Default,MainActor,#FFCC00,,VO/voice_memCommon, -sellerA,销售员A,Seller A,Seller A,,,,Default,MainActor,#FFCC00,,VO/voice_memCommon, -sellerB,销售员B,Seller B,Seller B,,,,Default,MainActor,#FFCC00,,VO/voice_memCommon, -sellerC,销售员C,Seller C,Seller C,,,,Default,MainActor,#FFCC00,,VO/voice_memCommon, - +gts,患者,Patient,患者,Пациент,Paciente,Paciente,Default,MainActor,#A4CD36,,VO/voice_GTS, +gts?,患者,Patient,患者,Пациент,Paciente,Paciente,Default,MainActor,#A4CD36,,VO/voice_GTS_broken, +gts??,患者??,Patient??,患者??,Пациент??,¿¿Paciente??,Paciente??,Default,MainActor,#E2C22B,,VO/voice_GTS, +classmate,同学,Classmate,同級生,Одноклассник,Compañero de clase,Colega,Default,Aside,,,VO/voice_memCommon, +jb,酒保,Bartender,バーテンダー,Бармен,Cantinero,Bartender,Default,MainActor,,,VO/voice_memCommon, +host,主持人,Host,司会者,Ведущий,Presentador,Apresentador,Default,MainActor,,,VO/voice_memCommon, +teacher,教师,Teacher,教師,Учитель,Maestro,Professor,Default,MainActor,#FFCC00,,VO/voice_memCommon, +sellerA,销售员A,Salesperson A,販売員A,Продавец А,Vendedor A,Vendedor A,Default,MainActor,#FFCC00,,VO/voice_memCommon, +sellerB,销售员B,Salesperson B,販売員B,Продавец Б,Vendedor B,Vendedor B,Default,MainActor,#FFCC00,,VO/voice_memCommon, +sellerC,销售员C,Salesperson C,販売員C,Продавец В,Vendedor C,Vendedor C,Default,MainActor,#FFCC00,,VO/voice_memCommon, diff --git a/Assets/Tests/Typography.meta b/Assets/Tests/Typography.meta new file mode 100644 index 000000000..42baaed09 --- /dev/null +++ b/Assets/Tests/Typography.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 480fa26171aa2e242bff1885e9f7df55 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Typography/PlayMode.meta b/Assets/Tests/Typography/PlayMode.meta new file mode 100644 index 000000000..a87e02f92 --- /dev/null +++ b/Assets/Tests/Typography/PlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ad75debdd4433f642891c0cbc24a0bb5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Typography/PlayMode/AibisDream.Typography.Tests.PlayMode.asmdef b/Assets/Tests/Typography/PlayMode/AibisDream.Typography.Tests.PlayMode.asmdef new file mode 100644 index 000000000..e6a19532a --- /dev/null +++ b/Assets/Tests/Typography/PlayMode/AibisDream.Typography.Tests.PlayMode.asmdef @@ -0,0 +1,22 @@ +{ + "name": "AibisDream.Typography.Tests.PlayMode", + "rootNamespace": "AibisDream.Typography.Tests.PlayMode", + "references": [ + "AibisDream.Typography.Runtime", + "Unity.TextMeshPro" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false, + "optionalUnityReferences": [ + "TestAssemblies" + ] +} diff --git a/Assets/Tests/Typography/PlayMode/AibisDream.Typography.Tests.PlayMode.asmdef.meta b/Assets/Tests/Typography/PlayMode/AibisDream.Typography.Tests.PlayMode.asmdef.meta new file mode 100644 index 000000000..fd082e360 --- /dev/null +++ b/Assets/Tests/Typography/PlayMode/AibisDream.Typography.Tests.PlayMode.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4bd93b18a8a4497478c36873cabf5355 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Typography/PlayMode/TypographyServicePlayModeTests.cs b/Assets/Tests/Typography/PlayMode/TypographyServicePlayModeTests.cs new file mode 100644 index 000000000..d00d9077c --- /dev/null +++ b/Assets/Tests/Typography/PlayMode/TypographyServicePlayModeTests.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using AibisDream.Framework; +using NUnit.Framework; +using TMPro; +using UnityEngine; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +namespace AibisDream.Typography.Tests.PlayMode +{ + public sealed class TypographyServicePlayModeTests + { + private readonly List createdObjects = new(); + + [UnitySetUp] + public IEnumerator SetUp() + { + TypographyService.ResetForTests(); + yield return null; + } + + [UnityTearDown] + public IEnumerator TearDown() + { + TypographyService.ResetForTests(); + foreach (Object created in createdObjects) + { + if (created != null) + Object.Destroy(created); + } + createdObjects.Clear(); + yield return null; + } + + [UnityTest] + public IEnumerator RuntimeCreatedText_AppliesCurrentProfile() + { + TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1.25f); + var loader = new ImmediateLoader(new Dictionary + { + [TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin + }); + TypographyService.ConfigureLoader(loader); + + Task loadTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); + yield return new WaitUntil(() => loadTask.IsCompleted); + Assert.That(loadTask.Result, Is.True); + + var go = new GameObject("RuntimeTypographyText", typeof(RectTransform)); + createdObjects.Add(go); + TextMeshProUGUI text = go.AddComponent(); + text.fontSize = 20f; + + Assert.That(TypographyService.Apply(text, TypographyRole.Body), Is.True); + Assert.That(text.fontSize, Is.EqualTo(25f)); + Assert.That(text.font, Is.EqualTo(TMP_Settings.defaultFontAsset)); + } + + [UnityTest] + public IEnumerator RegisteredBinding_SwitchesProfilesWithoutMetricAccumulationAndReleasesOldKey() + { + TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 0.5f); + TypographyProfile russian = CreateProfile(TypographyProfileKind.Russian, 0.75f); + var loader = new ImmediateLoader(new Dictionary + { + [TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin, + [TypographyLocaleMapping.GetProfileKey(LocaleKind.Ru)] = russian + }); + TypographyService.ConfigureLoader(loader); + + var go = new GameObject("BoundTypographyText", typeof(RectTransform)); + createdObjects.Add(go); + TextMeshProUGUI text = go.AddComponent(); + text.fontSize = 40f; + go.AddComponent(); + + Task latinTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); + yield return new WaitUntil(() => latinTask.IsCompleted); + Assert.That(text.fontSize, Is.EqualTo(20f)); + + Task russianTask = TypographyService.LoadAndActivateAsync(LocaleKind.Ru); + yield return new WaitUntil(() => russianTask.IsCompleted); + Assert.That(text.fontSize, Is.EqualTo(30f)); + Assert.That( + loader.ReleasedKeys, + Does.Contain(TypographyLocaleMapping.GetProfileKey(LocaleKind.En))); + + Task backTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); + yield return new WaitUntil(() => backTask.IsCompleted); + Assert.That(text.fontSize, Is.EqualTo(20f)); + } + + [UnityTest] + public IEnumerator PrefabOverride_UsesResolvedInstanceMetricsAsBaseline() + { + TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1f); + var loader = new ImmediateLoader(new Dictionary + { + [TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin + }); + TypographyService.ConfigureLoader(loader); + + Task loadTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); + yield return new WaitUntil(() => loadTask.IsCompleted); + Assert.That(loadTask.Result, Is.True); + + var go = new GameObject("PrefabOverrideTypographyText", typeof(RectTransform)); + createdObjects.Add(go); + go.SetActive(false); + TextMeshProUGUI text = go.AddComponent(); + LocalizedTypography binding = go.AddComponent(); + + const float sourceBaseline = 40f; + const float resolvedOverride = 36f; + SetPrivateField(binding, "baselineCaptured", true); + SetPrivateField(binding, "baseFontSize", sourceBaseline); + text.fontSize = resolvedOverride; + + go.SetActive(true); + yield return null; + + Assert.That(text.fontSize, Is.EqualTo(resolvedOverride)); + } + + [UnityTest] + public IEnumerator OverlappingLoads_LastRequestWins() + { + TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1f); + TypographyProfile japanese = CreateProfile(TypographyProfileKind.Japanese, 1f); + var loader = new DeferredLoader(); + TypographyService.ConfigureLoader(loader); + + Task latinTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); + Task japaneseTask = TypographyService.LoadAndActivateAsync(LocaleKind.Ja); + + loader.Complete(TypographyLocaleMapping.GetProfileKey(LocaleKind.Ja), japanese); + yield return new WaitUntil(() => japaneseTask.IsCompleted); + loader.Complete(TypographyLocaleMapping.GetProfileKey(LocaleKind.En), latin); + yield return new WaitUntil(() => latinTask.IsCompleted); + + Assert.That(TypographyService.CurrentProfile, Is.SameAs(japanese)); + Assert.That( + TypographyService.CurrentProfileKey, + Is.EqualTo(TypographyLocaleMapping.GetProfileKey(LocaleKind.Ja))); + Assert.That(latinTask.Result, Is.False); + } + + [UnityTest] + public IEnumerator OverlappingLoads_ForSameProfileDoNotReleaseTheWinningHandle() + { + TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1f); + var loader = new DeferredLoader(); + TypographyService.ConfigureLoader(loader); + + Task firstTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); + Task secondTask = TypographyService.LoadAndActivateAsync(LocaleKind.Es); + loader.Complete(TypographyLocaleMapping.GetProfileKey(LocaleKind.En), latin); + + yield return new WaitUntil(() => firstTask.IsCompleted && secondTask.IsCompleted); + Assert.That(firstTask.Result, Is.False); + Assert.That(secondTask.Result, Is.True); + Assert.That(TypographyService.CurrentProfile, Is.SameAs(latin)); + Assert.That(loader.ReleasedKeys, Is.Empty); + } + + [UnityTest] + public IEnumerator MissingProfile_KeepsTheLastValidProfile() + { + TypographyProfile latin = CreateProfile(TypographyProfileKind.Latin, 1f); + var loader = new ImmediateLoader(new Dictionary + { + [TypographyLocaleMapping.GetProfileKey(LocaleKind.En)] = latin + }); + TypographyService.ConfigureLoader(loader); + + Task validTask = TypographyService.LoadAndActivateAsync(LocaleKind.En); + yield return new WaitUntil(() => validTask.IsCompleted); + LogAssert.Expect( + LogType.Error, + "[TypographyService] 字体 Profile 不存在或加载失败:Typography/Profile/Japanese"); + Task missingTask = TypographyService.LoadAndActivateAsync(LocaleKind.Ja); + yield return new WaitUntil(() => missingTask.IsCompleted); + + Assert.That(validTask.Result, Is.True); + Assert.That(missingTask.Result, Is.False); + Assert.That(TypographyService.CurrentProfile, Is.SameAs(latin)); + Assert.That( + TypographyService.CurrentProfileKey, + Is.EqualTo(TypographyLocaleMapping.GetProfileKey(LocaleKind.En))); + } + + [UnityTest] + public IEnumerator LocalizationKit_SupportedLocalesLoadRealAddressableProfilesAndReleasePreviousHandles() + { + Type localizationKit = Type.GetType( + "AibisDream.Framework.LocalizationKit, Assembly-CSharp", + throwOnError: true); + Type resourceSystem = Type.GetType( + "AibisDream.Framework.ResourceSystem, Assembly-CSharp", + throwOnError: true); + MethodInfo switchLanguage = localizationKit.GetMethod( + "SwitchLanguageAsync", + BindingFlags.Public | BindingFlags.Static); + PropertyInfo currentLanguageCode = localizationKit.GetProperty( + "CurrentLanguageCode", + BindingFlags.Public | BindingFlags.Static); + PropertyInfo persistentLoader = resourceSystem.GetProperty( + "PersistentLoader", + BindingFlags.Public | BindingFlags.Static); + Assert.That(switchLanguage, Is.Not.Null); + Assert.That(currentLanguageCode, Is.Not.Null); + Assert.That(persistentLoader, Is.Not.Null); + + string originalLocale = (string)currentLanguageCode.GetValue(null); + var localeCases = new[] + { + (Code: "zh-Hans", Kind: LocaleKind.Cn), + (Code: "en", Kind: LocaleKind.En), + (Code: "ja-JP", Kind: LocaleKind.Ja), + (Code: "ru", Kind: LocaleKind.Ru), + (Code: "es", Kind: LocaleKind.Es), + (Code: "pt-BR", Kind: LocaleKind.PtBr) + }; + + foreach (var localeCase in localeCases) + { + var switchTask = (Task)switchLanguage.Invoke(null, new object[] { localeCase.Code }); + yield return new WaitUntil(() => switchTask.IsCompleted); + Assert.That(switchTask.IsFaulted, Is.False, localeCase.Code); + Assert.That(switchTask.Result, Is.True, localeCase.Code); + Assert.That( + TypographyService.CurrentProfileKey, + Is.EqualTo(TypographyLocaleMapping.GetProfileKey(localeCase.Kind)), + localeCase.Code); + + object loaderInstance = persistentLoader.GetValue(null); + Assert.That(loaderInstance, Is.Not.Null, localeCase.Code); + int handleCount = (int)loaderInstance.GetType() + .GetProperty("TotalHandleCount", BindingFlags.Public | BindingFlags.Instance) + .GetValue(loaderInstance); + Assert.That(handleCount, Is.EqualTo(1), localeCase.Code); + } + + var restoreTask = (Task)switchLanguage.Invoke(null, new object[] { originalLocale }); + yield return new WaitUntil(() => restoreTask.IsCompleted); + Assert.That(restoreTask.Result, Is.True); + } + + private TypographyProfile CreateProfile(TypographyProfileKind kind, float multiplier) + { + var profile = ScriptableObject.CreateInstance(); + createdObjects.Add(profile); + profile.Configure( + kind, + new[] + { + new TypographyStyle( + TypographyRole.Body, + TMP_Settings.defaultFontAsset, + fontSizeMultiplier: multiplier) + }); + return profile; + } + + private static void SetPrivateField(LocalizedTypography binding, string fieldName, T value) + { + FieldInfo field = typeof(LocalizedTypography).GetField( + fieldName, + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(field, Is.Not.Null, fieldName); + field.SetValue(binding, value); + } + + private sealed class ImmediateLoader : ITypographyProfileLoader + { + private readonly IReadOnlyDictionary profiles; + + public ImmediateLoader(IReadOnlyDictionary profiles) + { + this.profiles = profiles; + } + + public List ReleasedKeys { get; } = new(); + + public Task LoadAsync(string key) + { + profiles.TryGetValue(key, out TypographyProfile profile); + return Task.FromResult(profile); + } + + public void Release(string key) + { + ReleasedKeys.Add(key); + } + } + + private sealed class DeferredLoader : ITypographyProfileLoader + { + private readonly Dictionary> pending = new(); + + public List ReleasedKeys { get; } = new(); + + public Task LoadAsync(string key) + { + if (!pending.TryGetValue(key, out TaskCompletionSource source)) + { + source = new TaskCompletionSource(); + pending[key] = source; + } + return source.Task; + } + + public void Complete(string key, TypographyProfile profile) + { + pending[key].SetResult(profile); + } + + public void Release(string key) + { + ReleasedKeys.Add(key); + } + } + } +} diff --git a/Assets/Tests/Typography/PlayMode/TypographyServicePlayModeTests.cs.meta b/Assets/Tests/Typography/PlayMode/TypographyServicePlayModeTests.cs.meta new file mode 100644 index 000000000..683810e2b --- /dev/null +++ b/Assets/Tests/Typography/PlayMode/TypographyServicePlayModeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a0486afd10903e4f81fc8560089f9f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: