commit 92623a1bc8678319819e3300eddf1ccff41a55e9 Author: RCiBot Date: Thu Nov 12 12:28:40 2026 +0800 feat: establish mech extraction prototype with bilingual UI Track Unity source, scenes, editable assets, licenses, tools and QA evidence. Exclude generated caches, Windows builds and raw runtime logs. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ce0f2fe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*.cs] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +insert_final_newline = true +csharp_new_line_before_open_brace = all +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = false + +[*.{py,json,md,uss,tss}] +charset = utf-8 +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5a32c35 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,35 @@ +* text=auto + +# Unity text assets and source stay consistent across Windows and other editors. +*.cs text eol=lf +*.py text eol=lf +*.json text eol=lf +*.asmdef text eol=lf +*.inputactions text eol=lf +*.meta text eol=lf +*.unity text eol=lf +*.prefab text eol=lf +*.asset text eol=lf +*.mat text eol=lf +*.uss text eol=lf +*.tss text eol=lf +*.md text eol=lf +*.txt text eol=lf +.editorconfig text eol=lf +.gitignore text eol=lf +.gitattributes text eol=lf +.vsconfig text eol=lf + +# Unity writes a trailing space after empty serialized YAML values. +*.meta whitespace=-blank-at-eol +*.unity whitespace=-blank-at-eol +*.prefab whitespace=-blank-at-eol +*.asset whitespace=-blank-at-eol +*.mat whitespace=-blank-at-eol + +# Models, editable Blender sources, fonts and QA images are binary assets. +*.fbx binary +*.blend binary +*.ttf binary +*.otf binary +*.png binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..835daea --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Ll]ogs/ +/[Uu]ser[Ss]ettings/ +/[Bb]uilds/ +/MemoryCaptures/ +/Recordings/ +.vs/ +.idea/ +*.csproj +*.sln +*.slnx +*.user +*.pidb +*.booproj +*.opendb +*.VC.db +__pycache__/ +*.pyc +*.blend1 +*.blend2 +*.log +/.env +/.env.* +!/.env.example +.DS_Store +Thumbs.db diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000..f019fd0 --- /dev/null +++ b/.vsconfig @@ -0,0 +1,6 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.VisualStudio.Workload.ManagedGame" + ] +} diff --git a/Assets/Editor.meta b/Assets/Editor.meta new file mode 100644 index 0000000..4d9a6d1 --- /dev/null +++ b/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0a5846b24b5565e4e8d6db6eac469833 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Ironspore.Editor.asmdef b/Assets/Editor/Ironspore.Editor.asmdef new file mode 100644 index 0000000..5a7a304 --- /dev/null +++ b/Assets/Editor/Ironspore.Editor.asmdef @@ -0,0 +1 @@ +{"name":"Ironspore.Editor","references":["Ironspore.Runtime","Unity.InputSystem"],"includePlatforms":["Editor"]} diff --git a/Assets/Editor/Ironspore.Editor.asmdef.meta b/Assets/Editor/Ironspore.Editor.asmdef.meta new file mode 100644 index 0000000..b483b66 --- /dev/null +++ b/Assets/Editor/Ironspore.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b6c55e1782f936c42aca5fab7f44c7e2 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/LocalizationBuilder.cs b/Assets/Editor/LocalizationBuilder.cs new file mode 100644 index 0000000..b2fac9d --- /dev/null +++ b/Assets/Editor/LocalizationBuilder.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.TextCore.LowLevel; +using UnityEngine.TextCore.Text; +using UnityEngine.UIElements; + +namespace Ironspore.Editor +{ + public static class LocalizationBuilder + { + public const string CatalogPath = "Assets/Game/Localization/LocalizationCatalog.asset"; + const string FontPath = "Assets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf"; + const string FontAssetPath = "Assets/Game/UI/Fonts/GameFont.asset"; + + [MenuItem("Ironspore/Configure Localization")] + public static string Configure() + { + if (EditorApplication.isPlaying) + throw new InvalidOperationException("Stop play mode before configuring localization."); + Directory.CreateDirectory("Assets/Game/UI/Fonts"); + AssetDatabase.Refresh(); + var importer = AssetImporter.GetAtPath(FontPath) as TrueTypeFontImporter; + if (!importer) + throw new FileNotFoundException("Archived OFL font has not imported.", FontPath); + importer.fontTextureCase = FontTextureCase.Dynamic; + importer.fontSize = 40; + importer.includeFontData = true; + importer.SaveAndReimport(); + var font = AssetDatabase.LoadAssetAtPath(FontPath); + var catalog = AssetDatabase.LoadAssetAtPath(CatalogPath); + if (!catalog) + { + catalog = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(catalog, CatalogPath); + } + catalog.locales = new[] + { + new GameLocale { code = "zh-Hans", nativeName = "简体中文", table = AssetDatabase.LoadAssetAtPath("Assets/Game/Localization/Tables/zh-Hans.json") }, + new GameLocale { code = "en", nativeName = "English", table = AssetDatabase.LoadAssetAtPath("Assets/Game/Localization/Tables/en.json") } + }; + var uiFont = AssetDatabase.LoadAssetAtPath(FontAssetPath); + if (!uiFont) + { + uiFont = FontAsset.CreateFontAsset(font, 64, 7, GlyphRenderMode.SDFAA, 2048, 2048, AtlasPopulationMode.Dynamic, true); + uiFont.name = "GameFont"; + AssetDatabase.CreateAsset(uiFont, FontAssetPath); + uiFont.material.name = "GameFont Material"; + AssetDatabase.AddObjectToAsset(uiFont.material, uiFont); + } + uiFont.isMultiAtlasTexturesEnabled = true; + var characters = new HashSet(Enumerable.Range(32, 95).Select(x => (char)x)); + foreach (var locale in catalog.locales) + { + foreach (char c in locale.nativeName) + characters.Add(c); + foreach (var entry in GameLocalization.ReadTable(locale.table)) + foreach (char c in entry.Value) + if (!char.IsControl(c)) + characters.Add(c); + } + string absent; + uiFont.TryAddCharacters(new string(characters.OrderBy(x => x).ToArray()), out absent); + if (!string.IsNullOrEmpty(absent)) + throw new InvalidOperationException("Font is missing required glyphs: " + absent); + foreach (var atlas in uiFont.atlasTextures) + { + if (!atlas || AssetDatabase.Contains(atlas)) + continue; + atlas.name = "GameFont Atlas"; + AssetDatabase.AddObjectToAsset(atlas, uiFont); + } + EditorUtility.SetDirty(uiFont); + catalog.worldFont = font; + catalog.uiFont = uiFont; + EditorUtility.SetDirty(catalog); + var textSettings = AssetDatabase.LoadAssetAtPath("Assets/Game/UI/Fonts/GameTextSettings.asset"); + if (!textSettings) + { + textSettings = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(textSettings, "Assets/Game/UI/Fonts/GameTextSettings.asset"); + } + textSettings.fallbackFontAssets = new List { uiFont }; + textSettings.clearDynamicDataOnBuild = false; + EditorUtility.SetDirty(textSettings); + var panel = AssetDatabase.LoadAssetAtPath("Assets/Game/UI/GamePanelSettings.asset"); + panel.textSettings = textSettings; + EditorUtility.SetDirty(panel); + string previousScene = EditorSceneManager.GetActiveScene().path; + foreach (string name in new[] { "Bootstrap", "MechTestRange", "WarzonePrototype" }) + { + var scene = EditorSceneManager.OpenScene("Assets/Scenes/" + name + ".unity"); + var session = UnityEngine.Object.FindAnyObjectByType(); + session.localization = catalog; + EditorUtility.SetDirty(session); + EditorSceneManager.MarkSceneDirty(scene); + EditorSceneManager.SaveScene(scene); + } + AssetDatabase.SaveAssets(); + EditorSceneManager.OpenScene(string.IsNullOrEmpty(previousScene) ? "Assets/Scenes/Bootstrap.unity" : previousScene); + return "Configured two locales, " + characters.Count + " glyphs, UI Toolkit and TextMesh fonts, and all three scenes."; + } + } +} diff --git a/Assets/Editor/LocalizationBuilder.cs.meta b/Assets/Editor/LocalizationBuilder.cs.meta new file mode 100644 index 0000000..3ca38ad --- /dev/null +++ b/Assets/Editor/LocalizationBuilder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 56d5f94d61cfcd2408a8cfbf5b7ff206 \ No newline at end of file diff --git a/Assets/Editor/LocalizationSmoke.cs b/Assets/Editor/LocalizationSmoke.cs new file mode 100644 index 0000000..6f03e47 --- /dev/null +++ b/Assets/Editor/LocalizationSmoke.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Ironspore.Editor +{ + // Editor-only integration checks and real Game View captures, invoked through Unity MCP. + public static class LocalizationSmoke + { + const string Folder = "Docs/QA/Localization"; + static readonly List steps = new List(); + static readonly Dictionary rangeFiles = new Dictionary(); + static Result result; + static GameSession game; + static string preference, mainState; + static bool hadPreference; + static int initialChassis, step; + static int[] initialEquipment; + static float nextAt; + static VisualElement Root => game.Hud.GetComponent().rootVisualElement; + static string MainPath => Path.Combine(Application.persistentDataPath, "Ironspore", "profile.json"); + static byte[] mainBytes; + + [Serializable] + public sealed class Check + { + public string name; + public bool passed; + } + [Serializable] + public sealed class Result + { + public bool passed; + public int stepsCompleted; + public string error; + public List checks = new List(); + } + static void CheckThat(string name, bool condition) + { + result.checks.Add(new Check { name = name, passed = condition }); + if (!condition) + throw new InvalidOperationException(name); + } + static void Submit(string name) + { + var button = Root.Q