diff --git a/.github/instructions/code-review.instructions.md b/.github/instructions/code-review.instructions.md index dde750c7..fb00951c 100644 --- a/.github/instructions/code-review.instructions.md +++ b/.github/instructions/code-review.instructions.md @@ -43,8 +43,19 @@ Avoid LINQ in hot paths and UI code for performance: - Use array/list indexing instead of `.First()` / `.Last()` - LINQ allocates iterators and delegates - avoid in frequently called code +### 7. Unit Test Coverage +New features and new logic in non-core packages (JEngine.UI, JEngine.Util, and any future packages) MUST include unit tests: +- Target **93%+ code coverage** for all new/modified code +- **Applies to**: All `Packages/com.jasonxudeveloper.jengine.*` packages **except** `jengine.core` +- Prefer **EditMode tests** (`Tests/Editor/`) for most logic +- Use **PlayMode tests** (`Tests/Runtime/`) when runtime behavior requires it (MonoBehaviour lifecycle, scene loading, etc.) — these must run **non-interactively** (no user input, no manual scene setup) +- Cover: constructors, public API, fluent chaining, edge cases, event handlers +- Use reflection to test private methods (e.g. `OnAttachToPanel`, hover handlers) when they contain meaningful logic +- Verify tests exercise both happy paths and error/boundary conditions + ## Common Issues to Flag +- Missing or insufficient unit tests for new features - Missing XML documentation on public APIs - Direct `Debug.Log` (should use proper logging) - `Task` instead of `UniTask` diff --git a/.github/instructions/jengine.instructions.md b/.github/instructions/jengine.instructions.md index ad471088..976e1d87 100644 --- a/.github/instructions/jengine.instructions.md +++ b/.github/instructions/jengine.instructions.md @@ -102,6 +102,84 @@ internal class MyEditorClass } ``` +## Unit Testing + +### Scope +Unit tests are required for all non-core JEngine packages — i.e. any package under `Packages/com.jasonxudeveloper.jengine.*` **except** `jengine.core`. This includes JEngine.UI, JEngine.Util, and any future packages. + +### Coverage Requirement +New features and new logic MUST include unit tests targeting **93%+ code coverage**: +- All public methods, properties, and constructors +- Fluent API chaining +- Edge cases and error conditions +- Event handlers and callbacks (use reflection for private handlers) + +### Test Modes +- **EditMode tests** (`Tests/Editor/`): Preferred for most logic — fast, no scene required. +- **PlayMode tests** (`Tests/Runtime/`): Use when the test needs a running game loop, MonoBehaviour lifecycle, or scene loading. PlayMode tests **must run non-interactively** (no user input, no manual scene setup). Use `[UnityTest]` with `UniTask.ToCoroutine()` for async PlayMode tests. + +### Test Location +Tests mirror the source structure under each package's test folders: +``` +Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButton.cs + → Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonTests.cs + +Packages/com.jasonxudeveloper.jengine.util/Runtime/JAction.cs + → Packages/com.jasonxudeveloper.jengine.util/Tests/Editor/JActionTests.cs + +# PlayMode tests when runtime behavior requires it: +Packages/com.jasonxudeveloper.jengine.util/Runtime/SomeFeature.cs + → Packages/com.jasonxudeveloper.jengine.util/Tests/Runtime/SomeFeatureTests.cs +``` + +### EditMode Test Pattern +```csharp +[TestFixture] +public class MyComponentTests +{ + private MyComponent _component; + + [SetUp] + public void SetUp() + { + _component = new MyComponent(); + } + + [Test] + public void Constructor_Default_AddsBaseClass() + { + Assert.IsTrue(_component.ClassListContains("my-component")); + } +} +``` + +### PlayMode Test Pattern +PlayMode tests must be fully automated — no interactive input or manual scene setup: +```csharp +[TestFixture] +public class MyRuntimeTests +{ + [UnityTest] + public IEnumerator MyAsyncTest() => UniTask.ToCoroutine(async () => + { + var go = new GameObject(); + var component = go.AddComponent(); + await UniTask.DelayFrame(1); + Assert.IsTrue(component.IsInitialized); + Object.Destroy(go); + }); +} +``` + +### Testing Private Methods via Reflection +For private event handlers and internal styling methods: +```csharp +var method = typeof(MyComponent).GetMethod("OnMouseEnter", + BindingFlags.NonPublic | BindingFlags.Instance); +method.Invoke(_component, new object[] { null }); +Assert.AreEqual(expectedColor, _component.style.backgroundColor.value); +``` + ## Review Focus Areas When reviewing JEngine code, check: @@ -110,3 +188,4 @@ When reviewing JEngine code, check: 3. Resource cleanup (ScriptableObjects, events) 4. Thread safety for callback-accessed state 5. Proper namespace usage +6. Unit tests with 93%+ coverage for new features/logic diff --git a/UnityProject/Assets/HotUpdate/Compiled/AOT/Assembly-CSharp.dll.bytes b/UnityProject/Assets/HotUpdate/Compiled/AOT/Assembly-CSharp.dll.bytes index 3e08fe13..7561714f 100644 Binary files a/UnityProject/Assets/HotUpdate/Compiled/AOT/Assembly-CSharp.dll.bytes and b/UnityProject/Assets/HotUpdate/Compiled/AOT/Assembly-CSharp.dll.bytes differ diff --git a/UnityProject/Assets/HotUpdate/Compiled/AOT/JEngine.Core.dll.bytes b/UnityProject/Assets/HotUpdate/Compiled/AOT/JEngine.Core.dll.bytes index 5aa4c62d..42c5fdd8 100644 Binary files a/UnityProject/Assets/HotUpdate/Compiled/AOT/JEngine.Core.dll.bytes and b/UnityProject/Assets/HotUpdate/Compiled/AOT/JEngine.Core.dll.bytes differ diff --git a/UnityProject/Assets/HotUpdate/Compiled/HotUpdate.Code.dll.bytes b/UnityProject/Assets/HotUpdate/Compiled/HotUpdate.Code.dll.bytes index 2e4a6742..9c830aef 100644 Binary files a/UnityProject/Assets/HotUpdate/Compiled/HotUpdate.Code.dll.bytes and b/UnityProject/Assets/HotUpdate/Compiled/HotUpdate.Code.dll.bytes differ diff --git a/UnityProject/Assets/Obfuz/SymbolObfus/symbol-mapping.xml b/UnityProject/Assets/Obfuz/SymbolObfus/symbol-mapping.xml index 38ff1638..452886b4 100644 --- a/UnityProject/Assets/Obfuz/SymbolObfus/symbol-mapping.xml +++ b/UnityProject/Assets/Obfuz/SymbolObfus/symbol-mapping.xml @@ -306,12 +306,12 @@ - + - - + + - + @@ -418,8 +418,6 @@ - - @@ -477,11 +475,11 @@ - - + + @@ -549,13 +547,13 @@ - + - + @@ -574,8 +572,10 @@ + + @@ -594,16 +594,11 @@ - - - - - - - - - - + + + + + @@ -728,6 +723,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -777,6 +809,16 @@ + + + + + + + + + + @@ -854,6 +896,20 @@ + + + + + + + + + + + + + + @@ -934,6 +990,19 @@ + + + + + + + + + + + + + @@ -1013,6 +1082,17 @@ + + + + + + + + + + + @@ -1085,6 +1165,14 @@ + + + + + + + + @@ -1194,6 +1282,22 @@ + + + + + + + + + + + + + + + + @@ -1250,6 +1354,10 @@ + + + + diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/aa7811a04dda58d98435ef7170ce0828.bundle b/UnityProject/Assets/StreamingAssets/yoo/main/64a56ed51df1cee5f53c5ac4760ce454.bundle similarity index 99% rename from UnityProject/Assets/StreamingAssets/yoo/main/aa7811a04dda58d98435ef7170ce0828.bundle rename to UnityProject/Assets/StreamingAssets/yoo/main/64a56ed51df1cee5f53c5ac4760ce454.bundle index 5a927318..0953fe50 100644 Binary files a/UnityProject/Assets/StreamingAssets/yoo/main/aa7811a04dda58d98435ef7170ce0828.bundle and b/UnityProject/Assets/StreamingAssets/yoo/main/64a56ed51df1cee5f53c5ac4760ce454.bundle differ diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.bytes.meta b/UnityProject/Assets/StreamingAssets/yoo/main/64a56ed51df1cee5f53c5ac4760ce454.bundle.meta similarity index 74% rename from UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.bytes.meta rename to UnityProject/Assets/StreamingAssets/yoo/main/64a56ed51df1cee5f53c5ac4760ce454.bundle.meta index d2f2440d..a60c6c5a 100644 --- a/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.bytes.meta +++ b/UnityProject/Assets/StreamingAssets/yoo/main/64a56ed51df1cee5f53c5ac4760ce454.bundle.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e54223704c94c4bb5a1affe8058d803d +guid: 66733749d34dd4074a1a8f3086470ebf DefaultImporter: externalObjects: {} userData: diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/723671f97fbc4c6dfad84ca9c9664b26.bundle b/UnityProject/Assets/StreamingAssets/yoo/main/723671f97fbc4c6dfad84ca9c9664b26.bundle new file mode 100644 index 00000000..9c55c320 Binary files /dev/null and b/UnityProject/Assets/StreamingAssets/yoo/main/723671f97fbc4c6dfad84ca9c9664b26.bundle differ diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/7d008b589c7529b71f703107fc14b2a1.bundle.meta b/UnityProject/Assets/StreamingAssets/yoo/main/723671f97fbc4c6dfad84ca9c9664b26.bundle.meta similarity index 74% rename from UnityProject/Assets/StreamingAssets/yoo/main/7d008b589c7529b71f703107fc14b2a1.bundle.meta rename to UnityProject/Assets/StreamingAssets/yoo/main/723671f97fbc4c6dfad84ca9c9664b26.bundle.meta index f873a455..8ebcc9e1 100644 --- a/UnityProject/Assets/StreamingAssets/yoo/main/7d008b589c7529b71f703107fc14b2a1.bundle.meta +++ b/UnityProject/Assets/StreamingAssets/yoo/main/723671f97fbc4c6dfad84ca9c9664b26.bundle.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 958812791c38144aa81c7b1b1524183d +guid: 2ed375ade0e644184afb424f9005c348 DefaultImporter: externalObjects: {} userData: diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/7d008b589c7529b71f703107fc14b2a1.bundle b/UnityProject/Assets/StreamingAssets/yoo/main/7d008b589c7529b71f703107fc14b2a1.bundle deleted file mode 100644 index ca5fd514..00000000 Binary files a/UnityProject/Assets/StreamingAssets/yoo/main/7d008b589c7529b71f703107fc14b2a1.bundle and /dev/null differ diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/BuildinCatalog.bytes b/UnityProject/Assets/StreamingAssets/yoo/main/BuildinCatalog.bytes index f45f9ca0..95d8f8b8 100644 Binary files a/UnityProject/Assets/StreamingAssets/yoo/main/BuildinCatalog.bytes and b/UnityProject/Assets/StreamingAssets/yoo/main/BuildinCatalog.bytes differ diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/BuildinCatalog.json b/UnityProject/Assets/StreamingAssets/yoo/main/BuildinCatalog.json index 6ff07607..1552d82b 100644 --- a/UnityProject/Assets/StreamingAssets/yoo/main/BuildinCatalog.json +++ b/UnityProject/Assets/StreamingAssets/yoo/main/BuildinCatalog.json @@ -1,7 +1,7 @@ { "FileVersion": "1.0.0", "PackageName": "main", - "PackageVersion": "260202209", + "PackageVersion": "260208645", "Wrappers": [ { "BundleGUID": "427571ab4f40f72802eba023fca7a5e8", @@ -19,6 +19,14 @@ "BundleGUID": "578aebd507ab15c1f73f9a41b5962e15", "FileName": "578aebd507ab15c1f73f9a41b5962e15.bundle" }, + { + "BundleGUID": "64a56ed51df1cee5f53c5ac4760ce454", + "FileName": "64a56ed51df1cee5f53c5ac4760ce454.bundle" + }, + { + "BundleGUID": "723671f97fbc4c6dfad84ca9c9664b26", + "FileName": "723671f97fbc4c6dfad84ca9c9664b26.bundle" + }, { "BundleGUID": "743303a92561c811cf9e428d59f4eed5", "FileName": "743303a92561c811cf9e428d59f4eed5.bundle" @@ -27,18 +35,10 @@ "BundleGUID": "76c5d9ebda370e318d1d711bf29db391", "FileName": "76c5d9ebda370e318d1d711bf29db391.bundle" }, - { - "BundleGUID": "7d008b589c7529b71f703107fc14b2a1", - "FileName": "7d008b589c7529b71f703107fc14b2a1.bundle" - }, { "BundleGUID": "a99f206109faf44e637346323f255c84", "FileName": "a99f206109faf44e637346323f255c84.bundle" }, - { - "BundleGUID": "aa7811a04dda58d98435ef7170ce0828", - "FileName": "aa7811a04dda58d98435ef7170ce0828.bundle" - }, { "BundleGUID": "bd7a40533fe708ab184ec75ceac853ee", "FileName": "bd7a40533fe708ab184ec75ceac853ee.bundle" diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/main.version b/UnityProject/Assets/StreamingAssets/yoo/main/main.version index c18b9879..9adebe86 100644 --- a/UnityProject/Assets/StreamingAssets/yoo/main/main.version +++ b/UnityProject/Assets/StreamingAssets/yoo/main/main.version @@ -1 +1 @@ -260202209 \ No newline at end of file +260208645 \ No newline at end of file diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.hash b/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.hash deleted file mode 100644 index 684ea60d..00000000 --- a/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.hash +++ /dev/null @@ -1 +0,0 @@ -7f2ba677 \ No newline at end of file diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.bytes b/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.bytes similarity index 96% rename from UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.bytes rename to UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.bytes index 44af77e8..ff1b7f5c 100644 Binary files a/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.bytes and b/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.bytes differ diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.hash.meta b/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.bytes.meta similarity index 74% rename from UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.hash.meta rename to UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.bytes.meta index e64e2a6f..6896edd8 100644 --- a/UnityProject/Assets/StreamingAssets/yoo/main/main_260202209.hash.meta +++ b/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.bytes.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 24256b48ddcbd492892edc790e8f209c +guid: 2f3dfd9130e0f42069d8ea2f2ac59aa8 DefaultImporter: externalObjects: {} userData: diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.hash b/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.hash new file mode 100644 index 00000000..096bd8e4 --- /dev/null +++ b/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.hash @@ -0,0 +1 @@ +5faecfba \ No newline at end of file diff --git a/UnityProject/Assets/StreamingAssets/yoo/main/aa7811a04dda58d98435ef7170ce0828.bundle.meta b/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.hash.meta similarity index 74% rename from UnityProject/Assets/StreamingAssets/yoo/main/aa7811a04dda58d98435ef7170ce0828.bundle.meta rename to UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.hash.meta index 200b376c..c92f2611 100644 --- a/UnityProject/Assets/StreamingAssets/yoo/main/aa7811a04dda58d98435ef7170ce0828.bundle.meta +++ b/UnityProject/Assets/StreamingAssets/yoo/main/main_260208645.hash.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 40e342c01cb18476a95a4e9594d40050 +guid: edb8532e58ed74294bdb4cc3eefedd9c DefaultImporter: externalObjects: {} userData: 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..b7057f93 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, BootstrapText.SafeFormat(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, + BootstrapText.SafeFormat(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 = BootstrapText.SafeFormat(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, + BootstrapText.SafeFormat(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, BootstrapText.SafeFormat(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, BootstrapText.SafeFormat(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..24b43cc4 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/BootstrapText.cs @@ -0,0 +1,173 @@ +// 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}", + }; + + /// + /// Safe wrapper around that falls back + /// to a safe value if the user-edited format string is malformed or null. + /// + public static string SafeFormat(string template, params object[] args) + { + if (string.IsNullOrEmpty(template)) + { + return string.Empty; + } + + try + { + return string.Format(template, args); + } + catch (FormatException) + { + return template; + } + } + } +} 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/Components/Navigation/JTabView.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Navigation/JTabView.cs new file mode 100644 index 00000000..af026103 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Navigation/JTabView.cs @@ -0,0 +1,232 @@ +// JTabView.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.Collections.Generic; +using JEngine.UI.Editor.Theming; +using UnityEngine; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Navigation +{ + /// + /// A tabbed container that shows one content panel at a time. + /// Each tab has a button in the tab bar and an associated content element. + /// + public class JTabView : VisualElement + { + private readonly VisualElement _tabBar; + private readonly VisualElement _contentArea; + private readonly List