From ddc76452e95f4300bb4d835d7711469d4f943368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Sat, 7 Feb 2026 01:16:28 +1100 Subject: [PATCH 1/7] feat(core): add configurable Bootstrap text via BootstrapText struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a serializable BootstrapText struct that externalizes all ~30 user-facing strings from Bootstrap, enabling per-project localization and branding without modifying framework code. Closes #622 Co-Authored-By: Claude Opus 4.6 Signed-off-by: JasonXuDeveloper - 傑 --- .../Editor/CustomEditor/BootstrapEditor.cs | 50 ++++++ .../Runtime/Bootstrap.cs | 84 +++++----- .../Runtime/BootstrapText.cs | 152 ++++++++++++++++++ .../Runtime/BootstrapText.cs.meta | 11 ++ .../Editor/Internal/BootstrapEditorUI.cs | 111 +++++++++++++ .../Editor/Internal/BootstrapEditorUITests.cs | 34 ++++ 6 files changed, 406 insertions(+), 36 deletions(-) create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs.meta diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/BootstrapEditor.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/BootstrapEditor.cs index 6e5259d0..e7991c12 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/BootstrapEditor.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/BootstrapEditor.cs @@ -74,6 +74,9 @@ private VisualElement CreateDefaultInspectorGUI() // UI Settings Group CreateUISettingsGroup(); + // Text Settings Group + CreateTextSettingsGroup(); + UpdateFallbackServerVisibility(); return _root; @@ -440,6 +443,53 @@ private void CreateUISettingsGroup() _root.Add(uiGroup); } + private void CreateTextSettingsGroup() + { + var textGroup = CreateGroup("Text Settings"); + + var textProperty = serializedObject.FindProperty("text"); + + // Iterate child properties directly — no foldout + var iterator = textProperty.Copy(); + var endProperty = iterator.GetEndProperty(); + iterator.NextVisible(true); // enter children + while (!SerializedProperty.EqualContents(iterator, endProperty)) + { + var field = new PropertyField(iterator); + field.AddToClassList("form-control"); + textGroup.Add(field); + if (!iterator.NextVisible(false)) + break; + } + + // Reset to Defaults button + var resetRow = CreateFormRow(""); + var resetButton = new UnityEngine.UIElements.Button(() => + { + Undo.RecordObject(_bootstrap, "Reset Bootstrap Text to Defaults"); + var textProp = serializedObject.FindProperty("text"); + var defaults = BootstrapText.Default; + var fields = typeof(BootstrapText).GetFields(BindingFlags.Public | BindingFlags.Instance); + foreach (var field in fields) + { + var prop = textProp.FindPropertyRelative(field.Name); + if (prop != null && prop.propertyType == SerializedPropertyType.String) + { + prop.stringValue = (string)field.GetValue(defaults); + } + } + serializedObject.ApplyModifiedProperties(); + }); + resetButton.text = "Reset to Defaults"; + resetButton.AddToClassList("form-control"); + EditorUIUtils.MakeFormWidthButton(resetButton); + EditorUIUtils.SwitchButtonColor(resetButton, EditorUIUtils.ButtonType.Warning); + resetRow.Add(resetButton); + textGroup.Add(resetRow); + + _root.Add(textGroup); + } + #if UNITY_EDITOR private void CreateDevelopmentSettingsGroup() { diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs index af34abd7..4ea20f74 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs @@ -82,6 +82,9 @@ public partial class Bootstrap : MonoBehaviour public Button startButton; + [Header("Text Settings")] + [SerializeField] private BootstrapText text = BootstrapText.Default; + #if UNITY_EDITOR [Header("Development Settings")] [HideInInspector] public bool useEditorDevMode = true; @@ -194,6 +197,11 @@ private void Awake() }); } + private void Reset() + { + text = BootstrapText.Default; + } + private async void Initialize() { try @@ -202,7 +210,7 @@ private async void Initialize() } catch (Exception e) { - await Prompt.ShowDialogAsync("Error", $"Initialization failed: {e.Message}", "OK", null); + await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogInitFailed, e.Message), text.buttonOk, null); Application.Quit(); } } @@ -214,7 +222,7 @@ private async UniTask InitializeGame() { try { - updateStatusText.text = "Initializing..."; + updateStatusText.text = text.initializing; downloadProgressBar.gameObject.SetActive(false); YooAssets.Destroy(); @@ -229,20 +237,23 @@ private async UniTask InitializeGame() YooAssets.SetDefaultPackage(package); // Use the extracted common initialization function + var t = text; var packageInitCallbacks = new PackageInitializationCallbacks { - OnStatusUpdate = status => updateStatusText.text = GetStatusText(status), + OnStatusUpdate = status => updateStatusText.text = GetStatusText(status, t), OnVersionUpdate = version => versionText.text = $"v{Application.version}.{version}", OnDownloadPrompt = async (count, size) => - await Prompt.ShowDialogAsync("Notice", - $"Need to download {count} files, total size {size / 1024f / 1024f:F2}MB. Start download?", - "Download", "Cancel"), + await Prompt.ShowDialogAsync(t.dialogTitleNotice, + string.Format(t.dialogDownloadPrompt, count, $"{size / 1024f / 1024f:F2}"), + t.buttonDownload, t.buttonCancel), OnDownloadProgress = data => { if (updateStatusText != null) { - updateStatusText.text = - $"Downloading file {data.CurrentDownloadCount}/{data.TotalDownloadCount} ({data.CurrentDownloadBytes / 1024f / 1024f:F2}MB/{data.TotalDownloadBytes / 1024f / 1024f:F2}MB)"; + updateStatusText.text = string.Format(t.dialogDownloadProgress, + data.CurrentDownloadCount, data.TotalDownloadCount, + $"{data.CurrentDownloadBytes / 1024f / 1024f:F2}", + $"{data.TotalDownloadBytes / 1024f / 1024f:F2}"); } if (downloadProgressText != null) @@ -258,20 +269,20 @@ await Prompt.ShowDialogAsync("Notice", OnDownloadStart = () => { downloadProgressBar.gameObject.SetActive(true); - updateStatusText.text = "Downloading..."; + updateStatusText.text = t.downloading; downloadProgressText.text = ""; downloadProgressBar.value = 0f; }, OnDownloadComplete = () => { if (updateStatusText != null) - updateStatusText.text = "Download completed, loading..."; + updateStatusText.text = t.downloadCompletedLoading; if (downloadProgressText != null) downloadProgressText.text = "100%"; if (downloadProgressBar != null) downloadProgressBar.value = 1f; }, - OnError = async error => await Prompt.ShowDialogAsync("Warning", error.Message, "OK", null) + OnError = async error => await Prompt.ShowDialogAsync(t.dialogTitleWarning, error.Message, t.buttonOk, null) }; bool success = await UpdatePackage(package, packageInitCallbacks, encryptionOption); @@ -281,14 +292,14 @@ await Prompt.ShowDialogAsync("Notice", } // First supplement metadata - updateStatusText.text = "Loading code..."; + updateStatusText.text = text.loadingCode; await LoadMetadataForAOTAssemblies(); // Set dynamic key - updateStatusText.text = "Decrypting resources..."; + updateStatusText.text = text.decryptingResources; await SetUpDynamicSecret(); // Load hot update DLL - updateStatusText.text = "Loading code..."; + updateStatusText.text = text.loadingCode; #if UNITY_EDITOR var assemblies = AppDomain.CurrentDomain.GetAssemblies(); Assembly hotUpdateAss = null; @@ -316,10 +327,10 @@ await Prompt.ShowDialogAsync("Notice", #endif // Enter hot update scene - updateStatusText.text = "Loading scene..."; + updateStatusText.text = text.loadingScene; var sceneLoadCallbacks = new SceneLoadCallbacks { - OnStatusUpdate = status => updateStatusText.text = GetSceneLoadStatusText(status), + OnStatusUpdate = status => updateStatusText.text = GetSceneLoadStatusText(status, t), OnProgressUpdate = progress => { downloadProgressText.text = $"{Mathf.RoundToInt(progress * 100)}%"; @@ -327,8 +338,9 @@ await Prompt.ShowDialogAsync("Notice", }, OnError = async exception => { - await Prompt.ShowDialogAsync("Error", $"Scene loading failed: {exception.Message}", - "Retry", null); + await Prompt.ShowDialogAsync(t.dialogTitleError, + string.Format(t.dialogSceneLoadFailed, exception.Message), + t.buttonRetry, null); } }; downloadProgressBar.gameObject.SetActive(true); @@ -341,7 +353,7 @@ await Prompt.ShowDialogAsync("Error", $"Scene loading failed: {exception.Message catch (Exception ex) { Debug.LogError($"Initialization failed with exception: {ex}"); - await Prompt.ShowDialogAsync("Error", $"Exception occurred during initialization: {ex.Message}", "OK", "Cancel"); + await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogInitException, ex.Message), text.buttonOk, text.buttonCancel); // Continue the loop to retry } } @@ -352,7 +364,7 @@ private async UniTask LoadHotCode(Assembly hotUpdateAss) Type type = hotUpdateAss.GetType(hotUpdateClassName); if (type == null) { - await Prompt.ShowDialogAsync("Error", "Code exception, please contact customer service", null, "OK"); + await Prompt.ShowDialogAsync(text.dialogTitleError, text.dialogCodeException, null, text.buttonOk); Application.Quit(); return; } @@ -360,7 +372,7 @@ private async UniTask LoadHotCode(Assembly hotUpdateAss) var method = type.GetMethod(hotUpdateMethodName, BindingFlags.Public | BindingFlags.Static); if (method == null) { - await Prompt.ShowDialogAsync("Error", "Code exception, please contact customer service", null, "OK"); + await Prompt.ShowDialogAsync(text.dialogTitleError, text.dialogCodeException, null, text.buttonOk); Application.Quit(); return; } @@ -392,7 +404,7 @@ private async UniTask LoadHotCode(Assembly hotUpdateAss) catch (Exception e) { Debug.LogError($"Failed to invoke hot update method {hotUpdateMethodName}: {e}"); - await Prompt.ShowDialogAsync("Error", $"Function call failed: {e.Message}", "Exit", null); + await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogFunctionCallFailed, e.Message), text.buttonExit, null); Application.Quit(); } } @@ -677,29 +689,29 @@ private async UniTask UpdatePackageImpl(ResourcePackage package, } } - private static string GetStatusText(PackageInitializationStatus status) + private static string GetStatusText(PackageInitializationStatus status, BootstrapText text) { return status switch { - PackageInitializationStatus.InitializingPackage => "Initializing resource package...", - PackageInitializationStatus.GettingVersion => "Getting resource package version...", - PackageInitializationStatus.UpdatingManifest => "Updating resource manifest...", - PackageInitializationStatus.CheckingUpdate => "Checking resources to download...", - PackageInitializationStatus.DownloadingResources => "Downloading resources...", - PackageInitializationStatus.Completed => "Resource package initialization completed", - PackageInitializationStatus.Failed => "Initialization failed", - _ => "Unknown status" + PackageInitializationStatus.InitializingPackage => text.initializingPackage, + PackageInitializationStatus.GettingVersion => text.gettingVersion, + PackageInitializationStatus.UpdatingManifest => text.updatingManifest, + PackageInitializationStatus.CheckingUpdate => text.checkingUpdate, + PackageInitializationStatus.DownloadingResources => text.downloadingResources, + PackageInitializationStatus.Completed => text.packageCompleted, + PackageInitializationStatus.Failed => text.initializationFailed, + _ => text.unknownPackageStatus }; } - private static string GetSceneLoadStatusText(SceneLoadStatus status) + private static string GetSceneLoadStatusText(SceneLoadStatus status, BootstrapText text) { return status switch { - SceneLoadStatus.Loading => "Loading scene...", - SceneLoadStatus.Completed => "Scene loading completed", - SceneLoadStatus.Failed => "Scene loading failed", - _ => "Unknown status" + SceneLoadStatus.Loading => text.sceneLoading, + SceneLoadStatus.Completed => text.sceneCompleted, + SceneLoadStatus.Failed => text.sceneFailed, + _ => text.unknownSceneStatus }; } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs new file mode 100644 index 00000000..4be56bd0 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs @@ -0,0 +1,152 @@ +// BootstrapText.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using UnityEngine; + +namespace JEngine.Core +{ + /// + /// All user-facing strings shown by during initialization. + /// Customize per-project for localization or branding. + /// + [Serializable] + public struct BootstrapText + { + // ── Package Initialization Status ── + + [Header("Package Initialization Status")] + public string initializingPackage; + public string gettingVersion; + public string updatingManifest; + public string checkingUpdate; + public string downloadingResources; + public string packageCompleted; + public string initializationFailed; + public string unknownPackageStatus; + + // ── Scene Load Status ── + + [Header("Scene Load Status")] + public string sceneLoading; + public string sceneCompleted; + public string sceneFailed; + public string unknownSceneStatus; + + // ── Inline Status ── + + [Header("Inline Status")] + public string initializing; + public string downloading; + public string downloadCompletedLoading; + public string loadingCode; + public string decryptingResources; + public string loadingScene; + + // ── Dialog Titles ── + + [Header("Dialog Titles")] + public string dialogTitleError; + public string dialogTitleWarning; + public string dialogTitleNotice; + + // ── Dialog Buttons ── + + [Header("Dialog Buttons")] + public string buttonOk; + public string buttonCancel; + public string buttonDownload; + public string buttonRetry; + public string buttonExit; + + // ── Dialog Content (format strings) ── + + [Header("Dialog Content")] + [Tooltip("{0} = error message")] + public string dialogInitFailed; + + [Tooltip("{0} = file count, {1} = total size in MB")] + public string dialogDownloadPrompt; + + [Tooltip("{0} = current count, {1} = total count, {2} = current MB, {3} = total MB")] + public string dialogDownloadProgress; + + [Tooltip("{0} = error message")] + public string dialogSceneLoadFailed; + + [Tooltip("{0} = exception message")] + public string dialogInitException; + + public string dialogCodeException; + + [Tooltip("{0} = error message")] + public string dialogFunctionCallFailed; + + /// + /// Default English text matching the original hardcoded strings. + /// + public static readonly BootstrapText Default = new() + { + initializingPackage = "Initializing resource package...", + gettingVersion = "Getting resource package version...", + updatingManifest = "Updating resource manifest...", + checkingUpdate = "Checking resources to download...", + downloadingResources = "Downloading resources...", + packageCompleted = "Resource package initialization completed", + initializationFailed = "Initialization failed", + unknownPackageStatus = "Unknown status", + + sceneLoading = "Loading scene...", + sceneCompleted = "Scene loading completed", + sceneFailed = "Scene loading failed", + unknownSceneStatus = "Unknown status", + + initializing = "Initializing...", + downloading = "Downloading...", + downloadCompletedLoading = "Download completed, loading...", + loadingCode = "Loading code...", + decryptingResources = "Decrypting resources...", + loadingScene = "Loading scene...", + + dialogTitleError = "Error", + dialogTitleWarning = "Warning", + dialogTitleNotice = "Notice", + + buttonOk = "OK", + buttonCancel = "Cancel", + buttonDownload = "Download", + buttonRetry = "Retry", + buttonExit = "Exit", + + dialogInitFailed = "Initialization failed: {0}", + dialogDownloadPrompt = "Need to download {0} files, total size {1}MB. Start download?", + dialogDownloadProgress = "Downloading file {0}/{1} ({2}MB/{3}MB)", + dialogSceneLoadFailed = "Scene loading failed: {0}", + dialogInitException = "Exception occurred during initialization: {0}", + dialogCodeException = "Code exception, please contact customer service", + dialogFunctionCallFailed = "Function call failed: {0}", + }; + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs.meta new file mode 100644 index 00000000..ec5206f4 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 159bc6ca939304dc88e4729dfd960ea5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs index 6ea9b9e1..05578c5b 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs @@ -101,6 +101,9 @@ public static VisualElement CreateInspector(SerializedObject serializedObject, B // UI Settings content.Add(CreateUISettingsSection()); + // Text Settings + content.Add(CreateTextSettingsSection()); + container.Add(content); root.Add(container); @@ -150,6 +153,7 @@ private static void OnUndoRedo() content.Add(CreateAssetSettingsSection()); content.Add(CreateSecuritySettingsSection()); content.Add(CreateUISettingsSection()); + content.Add(CreateTextSettingsSection()); container.Add(content); _currentRoot.Add(container); @@ -438,6 +442,113 @@ private static VisualElement CreateUISettingsSection() return section; } + private static VisualElement CreateTextSettingsSection() + { + var section = new JSection("Text Settings"); + + var textProperty = _serializedObject.FindProperty("text"); + + // Package Initialization Status + AddTextSubHeader(section, "Package Initialization Status"); + AddTextField(section, textProperty, "initializingPackage", "Initializing"); + AddTextField(section, textProperty, "gettingVersion", "Getting Version"); + AddTextField(section, textProperty, "updatingManifest", "Updating Manifest"); + AddTextField(section, textProperty, "checkingUpdate", "Checking Update"); + AddTextField(section, textProperty, "downloadingResources", "Downloading"); + AddTextField(section, textProperty, "packageCompleted", "Completed"); + AddTextField(section, textProperty, "initializationFailed", "Failed"); + AddTextField(section, textProperty, "unknownPackageStatus", "Unknown Status"); + + // Scene Load Status + AddTextSubHeader(section, "Scene Load Status"); + AddTextField(section, textProperty, "sceneLoading", "Loading"); + AddTextField(section, textProperty, "sceneCompleted", "Completed"); + AddTextField(section, textProperty, "sceneFailed", "Failed"); + AddTextField(section, textProperty, "unknownSceneStatus", "Unknown Status"); + + // Inline Status + AddTextSubHeader(section, "Inline Status"); + AddTextField(section, textProperty, "initializing", "Initializing"); + AddTextField(section, textProperty, "downloading", "Downloading"); + AddTextField(section, textProperty, "downloadCompletedLoading", "Download Done"); + AddTextField(section, textProperty, "loadingCode", "Loading Code"); + AddTextField(section, textProperty, "decryptingResources", "Decrypting"); + AddTextField(section, textProperty, "loadingScene", "Loading Scene"); + + // Dialog Titles + AddTextSubHeader(section, "Dialog Titles"); + AddTextField(section, textProperty, "dialogTitleError", "Error Title"); + AddTextField(section, textProperty, "dialogTitleWarning", "Warning Title"); + AddTextField(section, textProperty, "dialogTitleNotice", "Notice Title"); + + // Dialog Buttons + AddTextSubHeader(section, "Dialog Buttons"); + AddTextField(section, textProperty, "buttonOk", "OK"); + AddTextField(section, textProperty, "buttonCancel", "Cancel"); + AddTextField(section, textProperty, "buttonDownload", "Download"); + AddTextField(section, textProperty, "buttonRetry", "Retry"); + AddTextField(section, textProperty, "buttonExit", "Exit"); + + // Dialog Content + AddTextSubHeader(section, "Dialog Content (Format Strings)"); + AddTextField(section, textProperty, "dialogInitFailed", "Init Failed"); + AddTextField(section, textProperty, "dialogDownloadPrompt", "Download Prompt"); + AddTextField(section, textProperty, "dialogDownloadProgress", "Download Progress"); + AddTextField(section, textProperty, "dialogSceneLoadFailed", "Scene Failed"); + AddTextField(section, textProperty, "dialogInitException", "Init Exception"); + AddTextField(section, textProperty, "dialogCodeException", "Code Exception"); + AddTextField(section, textProperty, "dialogFunctionCallFailed", "Call Failed"); + + // Reset to Defaults button + var resetButton = new JButton("Reset to Defaults", () => + { + Undo.RecordObject(_bootstrap, "Reset Bootstrap Text to Defaults"); + var textProp = _serializedObject.FindProperty("text"); + var defaults = BootstrapText.Default; + var fields = typeof(BootstrapText).GetFields( + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + foreach (var field in fields) + { + var prop = textProp.FindPropertyRelative(field.Name); + if (prop != null && prop.propertyType == SerializedPropertyType.String) + { + prop.stringValue = (string)field.GetValue(defaults); + } + } + _serializedObject.ApplyModifiedProperties(); + }, ButtonVariant.Warning); + section.Add(resetButton); + + return section; + } + + private static void AddTextSubHeader(JSection section, string title) + { + var header = new Label(title); + header.style.fontSize = Tokens.FontSize.Sm; + header.style.color = Tokens.Colors.TextMuted; + header.style.unityFontStyleAndWeight = FontStyle.Bold; + header.style.marginTop = Tokens.Spacing.MD; + header.style.marginBottom = Tokens.Spacing.Xs; + header.style.paddingBottom = Tokens.Spacing.Xs; + header.style.borderBottomWidth = 1; + header.style.borderBottomColor = Tokens.Colors.BorderSubtle; + section.Add(header); + } + + private static void AddTextField(JSection section, SerializedProperty parentProperty, + string fieldName, string label) + { + var prop = parentProperty.FindPropertyRelative(fieldName); + var textField = new JTextField(prop.stringValue); + textField.RegisterValueChangedCallback(evt => + { + prop.stringValue = evt.newValue; + _serializedObject.ApplyModifiedProperties(); + }); + section.Add(new JFormField(label, textField)); + } + private static void UpdateFallbackVisibility() { if (_fallbackContainer != null) diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs index 8aa9626c..0430678b 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs @@ -155,6 +155,40 @@ public void CreateInspector_ContainsUISettingsSection() Assert.IsNotNull(section); } + [Test] + public void CreateInspector_ContainsTextSettingsSection() + { + var root = BootstrapEditorUI.CreateInspector(_serializedObject, _bootstrap); + + var section = FindSectionByTitle(root, "Text Settings"); + Assert.IsNotNull(section); + } + + [Test] + public void TextSettings_ContainsFormFields() + { + var root = BootstrapEditorUI.CreateInspector(_serializedObject, _bootstrap); + + var section = FindSectionByTitle(root, "Text Settings"); + var formFields = section?.Query().ToList(); + + Assert.IsNotNull(formFields); + // Should have all 30 text fields + Assert.GreaterOrEqual(formFields.Count, 30); + } + + [Test] + public void TextSettings_ContainsResetButton() + { + var root = BootstrapEditorUI.CreateInspector(_serializedObject, _bootstrap); + + var section = FindSectionByTitle(root, "Text Settings"); + var buttons = section?.Query().ToList(); + + Assert.IsNotNull(buttons); + Assert.GreaterOrEqual(buttons.Count, 1); + } + #endregion #region JToggleButton Tests From 3d4e791548f1c3819b8852cded073ec04eb397de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Sat, 7 Feb 2026 01:40:02 +1100 Subject: [PATCH 2/7] fix(core): address code review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add BootstrapText.SafeFormat helper with try/catch fallback for malformed user-edited format strings - Replace all string.Format calls with SafeFormat in Bootstrap.cs - Use nameof(BootstrapText.*) instead of string literals in editor UI - Use BindProperty for JTextField binding (proper undo/prefab support) - Add tests for SafeFormat, Default field validation, exact field count, sub-headers, and reset logic Co-Authored-By: Claude Opus 4.6 Signed-off-by: JasonXuDeveloper - 傑 --- .../Runtime/Bootstrap.cs | 12 +- .../Runtime/BootstrapText.cs | 16 +++ .../Editor/Internal/BootstrapEditorUI.cs | 74 +++++----- .../Editor/Internal/BootstrapEditorUITests.cs | 136 +++++++++++++++++- 4 files changed, 189 insertions(+), 49 deletions(-) diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs index 4ea20f74..b7057f93 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs @@ -210,7 +210,7 @@ private async void Initialize() } catch (Exception e) { - await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogInitFailed, e.Message), text.buttonOk, null); + await Prompt.ShowDialogAsync(text.dialogTitleError, BootstrapText.SafeFormat(text.dialogInitFailed, e.Message), text.buttonOk, null); Application.Quit(); } } @@ -244,13 +244,13 @@ private async UniTask InitializeGame() OnVersionUpdate = version => versionText.text = $"v{Application.version}.{version}", OnDownloadPrompt = async (count, size) => await Prompt.ShowDialogAsync(t.dialogTitleNotice, - string.Format(t.dialogDownloadPrompt, count, $"{size / 1024f / 1024f:F2}"), + BootstrapText.SafeFormat(t.dialogDownloadPrompt, count, $"{size / 1024f / 1024f:F2}"), t.buttonDownload, t.buttonCancel), OnDownloadProgress = data => { if (updateStatusText != null) { - updateStatusText.text = string.Format(t.dialogDownloadProgress, + updateStatusText.text = BootstrapText.SafeFormat(t.dialogDownloadProgress, data.CurrentDownloadCount, data.TotalDownloadCount, $"{data.CurrentDownloadBytes / 1024f / 1024f:F2}", $"{data.TotalDownloadBytes / 1024f / 1024f:F2}"); @@ -339,7 +339,7 @@ await Prompt.ShowDialogAsync(t.dialogTitleNotice, OnError = async exception => { await Prompt.ShowDialogAsync(t.dialogTitleError, - string.Format(t.dialogSceneLoadFailed, exception.Message), + BootstrapText.SafeFormat(t.dialogSceneLoadFailed, exception.Message), t.buttonRetry, null); } }; @@ -353,7 +353,7 @@ await Prompt.ShowDialogAsync(t.dialogTitleError, catch (Exception ex) { Debug.LogError($"Initialization failed with exception: {ex}"); - await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogInitException, ex.Message), text.buttonOk, text.buttonCancel); + await Prompt.ShowDialogAsync(text.dialogTitleError, BootstrapText.SafeFormat(text.dialogInitException, ex.Message), text.buttonOk, text.buttonCancel); // Continue the loop to retry } } @@ -404,7 +404,7 @@ private async UniTask LoadHotCode(Assembly hotUpdateAss) catch (Exception e) { Debug.LogError($"Failed to invoke hot update method {hotUpdateMethodName}: {e}"); - await Prompt.ShowDialogAsync(text.dialogTitleError, string.Format(text.dialogFunctionCallFailed, e.Message), text.buttonExit, null); + await Prompt.ShowDialogAsync(text.dialogTitleError, BootstrapText.SafeFormat(text.dialogFunctionCallFailed, e.Message), text.buttonExit, null); Application.Quit(); } } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs index 4be56bd0..81bca1fa 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs @@ -148,5 +148,21 @@ public struct BootstrapText dialogCodeException = "Code exception, please contact customer service", dialogFunctionCallFailed = "Function call failed: {0}", }; + + /// + /// Safe wrapper around that falls back + /// to the raw template if the user-edited format string is malformed. + /// + public static string SafeFormat(string template, params object[] args) + { + try + { + return string.Format(template, args); + } + catch (FormatException) + { + return template; + } + } } } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs index 05578c5b..dd6cc22b 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs @@ -450,54 +450,54 @@ private static VisualElement CreateTextSettingsSection() // Package Initialization Status AddTextSubHeader(section, "Package Initialization Status"); - AddTextField(section, textProperty, "initializingPackage", "Initializing"); - AddTextField(section, textProperty, "gettingVersion", "Getting Version"); - AddTextField(section, textProperty, "updatingManifest", "Updating Manifest"); - AddTextField(section, textProperty, "checkingUpdate", "Checking Update"); - AddTextField(section, textProperty, "downloadingResources", "Downloading"); - AddTextField(section, textProperty, "packageCompleted", "Completed"); - AddTextField(section, textProperty, "initializationFailed", "Failed"); - AddTextField(section, textProperty, "unknownPackageStatus", "Unknown Status"); + AddTextField(section, textProperty, nameof(BootstrapText.initializingPackage), "Initializing"); + AddTextField(section, textProperty, nameof(BootstrapText.gettingVersion), "Getting Version"); + AddTextField(section, textProperty, nameof(BootstrapText.updatingManifest), "Updating Manifest"); + AddTextField(section, textProperty, nameof(BootstrapText.checkingUpdate), "Checking Update"); + AddTextField(section, textProperty, nameof(BootstrapText.downloadingResources), "Downloading"); + AddTextField(section, textProperty, nameof(BootstrapText.packageCompleted), "Completed"); + AddTextField(section, textProperty, nameof(BootstrapText.initializationFailed), "Failed"); + AddTextField(section, textProperty, nameof(BootstrapText.unknownPackageStatus), "Unknown Status"); // Scene Load Status AddTextSubHeader(section, "Scene Load Status"); - AddTextField(section, textProperty, "sceneLoading", "Loading"); - AddTextField(section, textProperty, "sceneCompleted", "Completed"); - AddTextField(section, textProperty, "sceneFailed", "Failed"); - AddTextField(section, textProperty, "unknownSceneStatus", "Unknown Status"); + AddTextField(section, textProperty, nameof(BootstrapText.sceneLoading), "Loading"); + AddTextField(section, textProperty, nameof(BootstrapText.sceneCompleted), "Completed"); + AddTextField(section, textProperty, nameof(BootstrapText.sceneFailed), "Failed"); + AddTextField(section, textProperty, nameof(BootstrapText.unknownSceneStatus), "Unknown Status"); // Inline Status AddTextSubHeader(section, "Inline Status"); - AddTextField(section, textProperty, "initializing", "Initializing"); - AddTextField(section, textProperty, "downloading", "Downloading"); - AddTextField(section, textProperty, "downloadCompletedLoading", "Download Done"); - AddTextField(section, textProperty, "loadingCode", "Loading Code"); - AddTextField(section, textProperty, "decryptingResources", "Decrypting"); - AddTextField(section, textProperty, "loadingScene", "Loading Scene"); + AddTextField(section, textProperty, nameof(BootstrapText.initializing), "Initializing"); + AddTextField(section, textProperty, nameof(BootstrapText.downloading), "Downloading"); + AddTextField(section, textProperty, nameof(BootstrapText.downloadCompletedLoading), "Download Done"); + AddTextField(section, textProperty, nameof(BootstrapText.loadingCode), "Loading Code"); + AddTextField(section, textProperty, nameof(BootstrapText.decryptingResources), "Decrypting"); + AddTextField(section, textProperty, nameof(BootstrapText.loadingScene), "Loading Scene"); // Dialog Titles AddTextSubHeader(section, "Dialog Titles"); - AddTextField(section, textProperty, "dialogTitleError", "Error Title"); - AddTextField(section, textProperty, "dialogTitleWarning", "Warning Title"); - AddTextField(section, textProperty, "dialogTitleNotice", "Notice Title"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogTitleError), "Error Title"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogTitleWarning), "Warning Title"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogTitleNotice), "Notice Title"); // Dialog Buttons AddTextSubHeader(section, "Dialog Buttons"); - AddTextField(section, textProperty, "buttonOk", "OK"); - AddTextField(section, textProperty, "buttonCancel", "Cancel"); - AddTextField(section, textProperty, "buttonDownload", "Download"); - AddTextField(section, textProperty, "buttonRetry", "Retry"); - AddTextField(section, textProperty, "buttonExit", "Exit"); + AddTextField(section, textProperty, nameof(BootstrapText.buttonOk), "OK"); + AddTextField(section, textProperty, nameof(BootstrapText.buttonCancel), "Cancel"); + AddTextField(section, textProperty, nameof(BootstrapText.buttonDownload), "Download"); + AddTextField(section, textProperty, nameof(BootstrapText.buttonRetry), "Retry"); + AddTextField(section, textProperty, nameof(BootstrapText.buttonExit), "Exit"); // Dialog Content AddTextSubHeader(section, "Dialog Content (Format Strings)"); - AddTextField(section, textProperty, "dialogInitFailed", "Init Failed"); - AddTextField(section, textProperty, "dialogDownloadPrompt", "Download Prompt"); - AddTextField(section, textProperty, "dialogDownloadProgress", "Download Progress"); - AddTextField(section, textProperty, "dialogSceneLoadFailed", "Scene Failed"); - AddTextField(section, textProperty, "dialogInitException", "Init Exception"); - AddTextField(section, textProperty, "dialogCodeException", "Code Exception"); - AddTextField(section, textProperty, "dialogFunctionCallFailed", "Call Failed"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogInitFailed), "Init Failed"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogDownloadPrompt), "Download Prompt"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogDownloadProgress), "Download Progress"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogSceneLoadFailed), "Scene Failed"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogInitException), "Init Exception"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogCodeException), "Code Exception"); + AddTextField(section, textProperty, nameof(BootstrapText.dialogFunctionCallFailed), "Call Failed"); // Reset to Defaults button var resetButton = new JButton("Reset to Defaults", () => @@ -540,12 +540,8 @@ private static void AddTextField(JSection section, SerializedProperty parentProp string fieldName, string label) { var prop = parentProperty.FindPropertyRelative(fieldName); - var textField = new JTextField(prop.stringValue); - textField.RegisterValueChangedCallback(evt => - { - prop.stringValue = evt.newValue; - _serializedObject.ApplyModifiedProperties(); - }); + var textField = new JTextField(); + textField.BindProperty(prop); section.Add(new JFormField(label, textField)); } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs index 0430678b..bee0f638 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/BootstrapEditorUITests.cs @@ -165,7 +165,7 @@ public void CreateInspector_ContainsTextSettingsSection() } [Test] - public void TextSettings_ContainsFormFields() + public void TextSettings_ContainsExactFieldCount() { var root = BootstrapEditorUI.CreateInspector(_serializedObject, _bootstrap); @@ -173,8 +173,31 @@ public void TextSettings_ContainsFormFields() var formFields = section?.Query().ToList(); Assert.IsNotNull(formFields); - // Should have all 30 text fields - Assert.GreaterOrEqual(formFields.Count, 30); + // Must match BootstrapText public instance field count exactly + var expectedCount = typeof(BootstrapText) + .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Length; + Assert.AreEqual(expectedCount, formFields.Count); + } + + [Test] + public void TextSettings_ContainsSubHeaders() + { + var root = BootstrapEditorUI.CreateInspector(_serializedObject, _bootstrap); + + var section = FindSectionByTitle(root, "Text Settings"); + var labels = section?.Query