From d9508dfd70fc4a19ef74e6d70095c4d1e9d66b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Wed, 28 Jan 2026 14:57:32 +1100 Subject: [PATCH 01/15] refactor(core): extract MessageBox to separate UI package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make core lightweight by extracting MessageBox with an abstraction layer: - Add Prompt abstraction in core for user dialogs (delegate-based) - Create new jengine.ui package for UI utilities - Move MessageBox from core to ui package (namespace: JEngine.UI) - Update Bootstrap to use Prompt.ShowDialogAsync instead of MessageBox - Add PromptInitializer.cs for easy UI integration Package structure after changes: - jengine.core: Independent hot update only, with Prompt abstraction - jengine.ui: Optional UI utilities (MessageBox) Users can customize prompt behavior by: 1. Using the default MessageBox via PromptInitializer 2. Implementing custom dialog providers This keeps core non-invasive (无侵入式) for hot updates while allowing UI customization through the optional ui package. Co-Authored-By: Claude Opus 4.5 Signed-off-by: JasonXuDeveloper - 傑 --- .github/workflows/pr-tests.yml | 13 +- .github/workflows/release.yml | 86 +++++- .../Assets/HotUpdate/Code/EntryPoint.cs | 2 +- .../HotUpdate/Code/HotUpdate.Code.asmdef | 4 +- UnityProject/Assets/Scripts.meta | 8 + .../Assets/Scripts/PromptInitializer.cs | 58 ++++ .../Assets/Scripts/PromptInitializer.cs.meta | 11 + .../Runtime/Bootstrap.cs | 21 +- .../Runtime/Misc.meta | 3 - .../Runtime/Misc/MessageBox.cs.meta | 3 - .../Runtime/Prompt.cs | 51 ++++ .../Runtime/Prompt.cs.meta | 11 + .../Runtime.meta | 8 + .../Runtime/JEngine.UI.asmdef | 17 ++ .../Runtime/JEngine.UI.asmdef.meta | 7 + .../Runtime}/MessageBox.cs | 49 ++- .../Runtime/MessageBox.cs.meta | 11 + .../Tests.meta | 8 + .../Tests/Editor.meta | 8 + .../Editor/JEngine.UI.Editor.Tests.asmdef | 25 ++ .../JEngine.UI.Editor.Tests.asmdef.meta | 7 + .../Tests/Editor/MessageBoxTests.cs | 278 ++++++++++++++++++ .../Tests/Editor/MessageBoxTests.cs.meta | 11 + .../package.json | 23 ++ .../package.json.meta | 7 + UnityProject/Packages/packages-lock.json | 8 + 26 files changed, 703 insertions(+), 35 deletions(-) create mode 100644 UnityProject/Assets/Scripts.meta create mode 100644 UnityProject/Assets/Scripts/PromptInitializer.cs create mode 100644 UnityProject/Assets/Scripts/PromptInitializer.cs.meta delete mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc.meta delete mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc/MessageBox.cs.meta create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs.meta create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime.meta create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/JEngine.UI.asmdef create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/JEngine.UI.asmdef.meta rename UnityProject/Packages/{com.jasonxudeveloper.jengine.core/Runtime/Misc => com.jasonxudeveloper.jengine.ui/Runtime}/MessageBox.cs (92%) create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs.meta create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests.meta create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor.meta create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngine.UI.Editor.Tests.asmdef create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngine.UI.Editor.Tests.asmdef.meta create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs.meta create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json create mode 100644 UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json.meta diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 53d10dd8..06887c3f 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -37,6 +37,7 @@ jobs: src: - 'UnityProject/Packages/com.jasonxudeveloper.jengine.core/**' - 'UnityProject/Packages/com.jasonxudeveloper.jengine.util/**' + - 'UnityProject/Packages/com.jasonxudeveloper.jengine.ui/**' - 'UnityProject/Assets/Tests/**' - '.github/workflows/unity-tests.yml' - '.github/workflows/pr-tests.yml' @@ -105,7 +106,17 @@ jobs: files: coverage/**/TestCoverageResults*.xml flags: util name: jengine-util - fail_ci_if_error: false + fail_ci_if_error: true + verbose: true + + - name: Upload coverage to Codecov (ui package) + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage/**/TestCoverageResults*.xml + flags: ui + name: jengine-ui + fail_ci_if_error: true verbose: true comment-results: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c60e71a3..212078ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,15 @@ on: description: 'New Util version (e.g., 1.0.1)' required: false type: string + release_ui: + description: 'Release JEngine.UI?' + required: true + type: boolean + default: false + ui_version: + description: 'New UI version (e.g., 1.0.0)' + required: false + type: string manual_changelog: description: 'Manual changelog entries (optional)' required: false @@ -37,6 +46,7 @@ jobs: outputs: core_version: ${{ steps.validate.outputs.core_version }} util_version: ${{ steps.validate.outputs.util_version }} + ui_version: ${{ steps.validate.outputs.ui_version }} release_tag: ${{ steps.validate.outputs.release_tag }} create_github_release: ${{ steps.validate.outputs.create_github_release }} @@ -48,7 +58,7 @@ jobs: id: validate run: | # Check at least one package is selected - if [ "${{ inputs.release_core }}" != "true" ] && [ "${{ inputs.release_util }}" != "true" ]; then + if [ "${{ inputs.release_core }}" != "true" ] && [ "${{ inputs.release_util }}" != "true" ] && [ "${{ inputs.release_ui }}" != "true" ]; then echo "Error: At least one package must be selected for release" exit 1 fi @@ -65,9 +75,11 @@ jobs: # Get current versions from package.json CURRENT_CORE_VERSION=$(jq -r '.version' UnityProject/Packages/com.jasonxudeveloper.jengine.core/package.json) CURRENT_UTIL_VERSION=$(jq -r '.version' UnityProject/Packages/com.jasonxudeveloper.jengine.util/package.json) + CURRENT_UI_VERSION=$(jq -r '.version' UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json) echo "Current Core version: $CURRENT_CORE_VERSION" echo "Current Util version: $CURRENT_UTIL_VERSION" + echo "Current UI version: $CURRENT_UI_VERSION" # Validate Core version if releasing if [ "${{ inputs.release_core }}" == "true" ]; then @@ -106,6 +118,24 @@ jobs: echo "util_version=$CURRENT_UTIL_VERSION" >> $GITHUB_OUTPUT fi + # Validate UI version if releasing + if [ "${{ inputs.release_ui }}" == "true" ]; then + if [ -z "${{ inputs.ui_version }}" ]; then + echo "Error: UI version is required when releasing UI package" + exit 1 + fi + validate_version "${{ inputs.ui_version }}" + + if [ "${{ inputs.ui_version }}" == "$CURRENT_UI_VERSION" ]; then + echo "Error: New UI version must be different from current version" + exit 1 + fi + + echo "ui_version=${{ inputs.ui_version }}" >> $GITHUB_OUTPUT + else + echo "ui_version=$CURRENT_UI_VERSION" >> $GITHUB_OUTPUT + fi + # Release tag always follows Core version # GitHub releases are only created when Core is released if [ "${{ inputs.release_core }}" == "true" ]; then @@ -276,13 +306,36 @@ jobs: # Build changelog CHANGELOG="" - # Add package release info (always show both versions for clarity) - if [ "${{ inputs.release_core }}" == "true" ] && [ "${{ inputs.release_util }}" == "true" ]; then - CHANGELOG="${CHANGELOG}**Released**: JEngine.Core v${{ needs.validate.outputs.core_version }}, JEngine.Util v${{ needs.validate.outputs.util_version }}\n\n" - elif [ "${{ inputs.release_core }}" == "true" ]; then - CHANGELOG="${CHANGELOG}**Released**: JEngine.Core v${{ needs.validate.outputs.core_version }} (Util remains v${{ needs.validate.outputs.util_version }})\n\n" + # Add package release info + RELEASED_PACKAGES="" + UNCHANGED_PACKAGES="" + + if [ "${{ inputs.release_core }}" == "true" ]; then + RELEASED_PACKAGES="${RELEASED_PACKAGES}JEngine.Core v${{ needs.validate.outputs.core_version }}, " else - CHANGELOG="${CHANGELOG}**Released**: JEngine.Util v${{ needs.validate.outputs.util_version }} (Core remains v${{ needs.validate.outputs.core_version }})\n\n" + UNCHANGED_PACKAGES="${UNCHANGED_PACKAGES}Core v${{ needs.validate.outputs.core_version }}, " + fi + + if [ "${{ inputs.release_util }}" == "true" ]; then + RELEASED_PACKAGES="${RELEASED_PACKAGES}JEngine.Util v${{ needs.validate.outputs.util_version }}, " + else + UNCHANGED_PACKAGES="${UNCHANGED_PACKAGES}Util v${{ needs.validate.outputs.util_version }}, " + fi + + if [ "${{ inputs.release_ui }}" == "true" ]; then + RELEASED_PACKAGES="${RELEASED_PACKAGES}JEngine.UI v${{ needs.validate.outputs.ui_version }}, " + else + UNCHANGED_PACKAGES="${UNCHANGED_PACKAGES}UI v${{ needs.validate.outputs.ui_version }}, " + fi + + # Remove trailing comma and space + RELEASED_PACKAGES=$(echo "$RELEASED_PACKAGES" | sed 's/, $//') + UNCHANGED_PACKAGES=$(echo "$UNCHANGED_PACKAGES" | sed 's/, $//') + + if [ -n "$UNCHANGED_PACKAGES" ]; then + CHANGELOG="${CHANGELOG}**Released**: ${RELEASED_PACKAGES} (${UNCHANGED_PACKAGES} unchanged)\n\n" + else + CHANGELOG="${CHANGELOG}**Released**: ${RELEASED_PACKAGES}\n\n" fi if [ -n "$BREAKING" ]; then @@ -341,6 +394,14 @@ jobs: mv /tmp/package.json UnityProject/Packages/com.jasonxudeveloper.jengine.util/package.json echo "✅ Updated Util package.json to v${{ needs.validate.outputs.util_version }}" + - name: Update UI package.json + if: inputs.release_ui == true + run: | + jq '.version = "${{ needs.validate.outputs.ui_version }}"' \ + UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json > /tmp/package.json + mv /tmp/package.json UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json + echo "✅ Updated UI package.json to v${{ needs.validate.outputs.ui_version }}" + # Update README files (only when releasing Core) - name: Update README.md if: inputs.release_core == true @@ -554,13 +615,17 @@ jobs: echo "✅ **JEngine.Util**: v${{ needs.validate.outputs.util_version }}" >> $GITHUB_STEP_SUMMARY fi + if [ "${{ inputs.release_ui }}" == "true" ]; then + echo "✅ **JEngine.UI**: v${{ needs.validate.outputs.ui_version }}" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY echo "🏷️ **Git Tag**: ${{ needs.validate.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY if [ "${{ needs.validate.outputs.create_github_release }}" == "true" ]; then echo "📋 **GitHub Release**: Will be created" >> $GITHUB_STEP_SUMMARY else - echo "ℹ️ **GitHub Release**: Not created (Util-only update)" >> $GITHUB_STEP_SUMMARY + echo "ℹ️ **GitHub Release**: Not created (non-Core update)" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY @@ -605,6 +670,7 @@ jobs: ```bash openupm add com.jasonxudeveloper.jengine.core openupm add com.jasonxudeveloper.jengine.util + openupm add com.jasonxudeveloper.jengine.ui # Optional: UI utilities ``` ## 📖 Documentation @@ -637,6 +703,10 @@ jobs: echo "✅ **JEngine.Util**: v${{ needs.validate.outputs.util_version }}" >> $GITHUB_STEP_SUMMARY fi + if [ "${{ inputs.release_ui }}" == "true" ]; then + echo "✅ **JEngine.UI**: v${{ needs.validate.outputs.ui_version }}" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY echo "**OpenUPM will automatically detect and build the packages within 10-15 minutes.**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/UnityProject/Assets/HotUpdate/Code/EntryPoint.cs b/UnityProject/Assets/HotUpdate/Code/EntryPoint.cs index f08bf709..ef69814a 100644 --- a/UnityProject/Assets/HotUpdate/Code/EntryPoint.cs +++ b/UnityProject/Assets/HotUpdate/Code/EntryPoint.cs @@ -26,8 +26,8 @@ using Cysharp.Threading.Tasks; using JEngine.Core; using JEngine.Core.Encrypt; -using JEngine.Core.Misc; using JEngine.Core.Update; +using JEngine.UI; using Obfuz; using UnityEngine; using UnityEngine.Scripting; diff --git a/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef b/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef index c5553745..02fd9a1d 100644 --- a/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef +++ b/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef @@ -11,7 +11,9 @@ "GUID:e34a5702dd353724aa315fb8011f08c3", "GUID:3fe1a3e70da50184f9897101cad7e4f2", "GUID:6055be8ebefd69e48b49212b09b47b2f", - "GUID:ba02d1bbd77cf4a0c8606d3e5cbbbe79" + "GUID:ba02d1bbd77cf4a0c8606d3e5cbbbe79", + "GUID:5c8e1f4d7a3b9e2c6f0d8a4b7e3c1f9d", + "GUID:5655bcbaa4dec434b86ecd07e2c6f24d" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/UnityProject/Assets/Scripts.meta b/UnityProject/Assets/Scripts.meta new file mode 100644 index 00000000..e25cddba --- /dev/null +++ b/UnityProject/Assets/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ef0c53a3e65534600a170f78ccdcd26e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Assets/Scripts/PromptInitializer.cs b/UnityProject/Assets/Scripts/PromptInitializer.cs new file mode 100644 index 00000000..f55349db --- /dev/null +++ b/UnityProject/Assets/Scripts/PromptInitializer.cs @@ -0,0 +1,58 @@ +// PromptInitializer.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. + +// ============================================================================ +// IMPORTANT: This script requires the JEngine.UI package to be installed. +// +// To use this script: +// 1. Add the JEngine.UI package via OpenUPM: +// openupm add com.jasonxudeveloper.jengine.ui +// +// 2. Or add to your manifest.json: +// "com.jasonxudeveloper.jengine.ui": "1.0.0" +// +// If you don't need MessageBox dialogs, you can: +// - Delete this script entirely (Bootstrap will log warnings and continue) +// - Implement your own custom dialog provider by assigning to Prompt.ShowDialogAsync +// ============================================================================ + +using JEngine.Core; +using JEngine.UI; +using UnityEngine; + +/// +/// Initializes the Prompt system to use MessageBox for dialogs. +/// This runs automatically before any scene loads. +/// +public static class PromptInitializer +{ + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Initialize() + { + // Register MessageBox as the dialog provider for JEngine.Core + Prompt.ShowDialogAsync = MessageBox.Show; + Debug.Log("[JEngine] Prompt system initialized with MessageBox provider."); + } +} diff --git a/UnityProject/Assets/Scripts/PromptInitializer.cs.meta b/UnityProject/Assets/Scripts/PromptInitializer.cs.meta new file mode 100644 index 00000000..5861562f --- /dev/null +++ b/UnityProject/Assets/Scripts/PromptInitializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1fb4f93a0b5b149cfa255f7c3f0a1697 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs index 03059a3e..af34abd7 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Bootstrap.cs @@ -31,7 +31,6 @@ using System.Threading.Tasks; using Cysharp.Threading.Tasks; using JEngine.Core.Encrypt; -using JEngine.Core.Misc; using JEngine.Core.Update; using Nino.Core; using Obfuz; @@ -203,7 +202,7 @@ private async void Initialize() } catch (Exception e) { - await MessageBox.Show("Error", $"Initialization failed: {e.Message}", no: null); + await Prompt.ShowDialogAsync("Error", $"Initialization failed: {e.Message}", "OK", null); Application.Quit(); } } @@ -235,9 +234,9 @@ private async UniTask InitializeGame() OnStatusUpdate = status => updateStatusText.text = GetStatusText(status), OnVersionUpdate = version => versionText.text = $"v{Application.version}.{version}", OnDownloadPrompt = async (count, size) => - await MessageBox.Show("Notice", + await Prompt.ShowDialogAsync("Notice", $"Need to download {count} files, total size {size / 1024f / 1024f:F2}MB. Start download?", - "Download"), + "Download", "Cancel"), OnDownloadProgress = data => { if (updateStatusText != null) @@ -272,7 +271,7 @@ await MessageBox.Show("Notice", if (downloadProgressBar != null) downloadProgressBar.value = 1f; }, - OnError = async error => await MessageBox.Show("Warning", error.Message, no: null) + OnError = async error => await Prompt.ShowDialogAsync("Warning", error.Message, "OK", null) }; bool success = await UpdatePackage(package, packageInitCallbacks, encryptionOption); @@ -328,8 +327,8 @@ await MessageBox.Show("Notice", }, OnError = async exception => { - await MessageBox.Show("Error", $"Scene loading failed: {exception.Message}", - ok: "Retry"); + await Prompt.ShowDialogAsync("Error", $"Scene loading failed: {exception.Message}", + "Retry", null); } }; downloadProgressBar.gameObject.SetActive(true); @@ -342,7 +341,7 @@ await MessageBox.Show("Error", $"Scene loading failed: {exception.Message}", catch (Exception ex) { Debug.LogError($"Initialization failed with exception: {ex}"); - await MessageBox.Show("Error", $"Exception occurred during initialization: {ex.Message}"); + await Prompt.ShowDialogAsync("Error", $"Exception occurred during initialization: {ex.Message}", "OK", "Cancel"); // Continue the loop to retry } } @@ -353,7 +352,7 @@ private async UniTask LoadHotCode(Assembly hotUpdateAss) Type type = hotUpdateAss.GetType(hotUpdateClassName); if (type == null) { - await MessageBox.Show("Error", "Code exception, please contact customer service", ok: null); + await Prompt.ShowDialogAsync("Error", "Code exception, please contact customer service", null, "OK"); Application.Quit(); return; } @@ -361,7 +360,7 @@ private async UniTask LoadHotCode(Assembly hotUpdateAss) var method = type.GetMethod(hotUpdateMethodName, BindingFlags.Public | BindingFlags.Static); if (method == null) { - await MessageBox.Show("Error", "Code exception, please contact customer service", ok: null); + await Prompt.ShowDialogAsync("Error", "Code exception, please contact customer service", null, "OK"); Application.Quit(); return; } @@ -393,7 +392,7 @@ private async UniTask LoadHotCode(Assembly hotUpdateAss) catch (Exception e) { Debug.LogError($"Failed to invoke hot update method {hotUpdateMethodName}: {e}"); - await MessageBox.Show("Error", $"Function call failed: {e.Message}", ok: "Exit", no: null); + await Prompt.ShowDialogAsync("Error", $"Function call failed: {e.Message}", "Exit", null); Application.Quit(); } } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc.meta deleted file mode 100644 index eaeb8455..00000000 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 6faa8ddc40274d2f9a95650400d32383 -timeCreated: 1758350708 \ No newline at end of file diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc/MessageBox.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc/MessageBox.cs.meta deleted file mode 100644 index af0eb400..00000000 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc/MessageBox.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: a57386ee91fd414e973cbf175247a5b7 -timeCreated: 1758194184 \ No newline at end of file diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs new file mode 100644 index 00000000..a4868c96 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs @@ -0,0 +1,51 @@ +// Prompt.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 Cysharp.Threading.Tasks; +using UnityEngine; + +namespace JEngine.Core +{ + /// + /// User prompt/dialog abstraction. Configure ShowDialogAsync to customize behavior. + /// + public static class Prompt + { + /// + /// Shows a dialog to the user and returns the result. + /// Parameters: title, content, okText (null to hide), noText (null to hide) + /// Returns: true if OK clicked, false if No/Cancel clicked + /// Default: logs warning and returns true (continues execution). + /// + public static Func> ShowDialogAsync = DefaultShowDialog; + + private static UniTask DefaultShowDialog(string title, string content, string ok, string no) + { + Debug.LogWarning($"[JEngine] Dialog provider not configured. {title}: {content}"); + return UniTask.FromResult(true); + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs.meta new file mode 100644 index 00000000..11173fac --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de19788e3bede46a0a533e49fecdd27e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime.meta new file mode 100644 index 00000000..b0ab2863 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ffdd2c7b248424797b5705ecee3572dd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/JEngine.UI.asmdef b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/JEngine.UI.asmdef new file mode 100644 index 00000000..016426b8 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/JEngine.UI.asmdef @@ -0,0 +1,17 @@ +{ + "name": "JEngine.UI", + "rootNamespace": "JEngine.UI", + "references": [ + "GUID:f51ebe6a0ceec4240a699833d6309b23", + "GUID:6055be8ebefd69e48b49212b09b47b2f" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/JEngine.UI.asmdef.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/JEngine.UI.asmdef.meta new file mode 100644 index 00000000..0d406cd9 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/JEngine.UI.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5655bcbaa4dec434b86ecd07e2c6f24d +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc/MessageBox.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs similarity index 92% rename from UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc/MessageBox.cs rename to UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs index 32523623..b66dbd63 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Misc/MessageBox.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs @@ -1,20 +1,20 @@ // MessageBox.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 @@ -25,13 +25,17 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Cysharp.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; using Object = UnityEngine.Object; -namespace JEngine.Core.Misc +[assembly: InternalsVisibleTo("JEngine.UI.Tests")] +[assembly: InternalsVisibleTo("JEngine.UI.Editor.Tests")] + +namespace JEngine.UI { public class MessageBox { @@ -106,6 +110,20 @@ private static GameObject Prefab private const int MaxPoolSize = 10; +#if UNITY_EDITOR + /// + /// Test handler to override Show() behavior. When set, Show() delegates to this handler + /// instead of creating UI. Set to null to restore normal behavior. + /// Only available in Editor for testing purposes. + /// + internal static Func> TestHandler; + + /// + /// When true, simulates prefab being unavailable. Used for testing "no prefab" error handling. + /// + internal static bool SimulateNoPrefab; +#endif + private TextMeshProUGUI _content; private TextMeshProUGUI _textNo; private TextMeshProUGUI _textOk; @@ -198,12 +216,29 @@ public static void CloseAll() /// If both buttons are null/empty, a default OK button will be shown to prevent unusable message box. public static UniTask Show(string title, string content, string ok = "OK", string no = "Cancel") { +#if UNITY_EDITOR + // Allow testing "no prefab" scenario + if (SimulateNoPrefab) + { + Debug.LogError("Cannot show MessageBox: Prefab is null"); + return UniTask.FromResult(false); + } +#endif + if (Prefab == null) { Debug.LogError("Cannot show MessageBox: Prefab is null"); return UniTask.FromResult(false); } +#if UNITY_EDITOR + // Allow tests to override behavior without UI (checked after prefab validation) + if (TestHandler != null) + { + return TestHandler(title, content, ok, no); + } +#endif + try { // Try to get from pool first @@ -448,4 +483,4 @@ private void Hide() _visible = false; } } -} \ No newline at end of file +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs.meta new file mode 100644 index 00000000..cacf2fbd --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e8123b7b4cf83481aa6e985016508f60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests.meta new file mode 100644 index 00000000..3efeda2d --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: daa65955d3aaf49ab89fd60948c3dfa6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor.meta new file mode 100644 index 00000000..4acb23b3 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 995c1ff148e9d435d8257d80988024aa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngine.UI.Editor.Tests.asmdef b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngine.UI.Editor.Tests.asmdef new file mode 100644 index 00000000..be4b78de --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngine.UI.Editor.Tests.asmdef @@ -0,0 +1,25 @@ +{ + "name": "JEngine.UI.Editor.Tests", + "rootNamespace": "JEngine.UI.Tests", + "references": [ + "JEngine.UI", + "UniTask", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngine.UI.Editor.Tests.asmdef.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngine.UI.Editor.Tests.asmdef.meta new file mode 100644 index 00000000..32f8f94c --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngine.UI.Editor.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 87d5f1ea41d314b32a96751c6d85cc20 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs new file mode 100644 index 00000000..bd12552d --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs @@ -0,0 +1,278 @@ +// MessageBoxTests.cs +// EditMode unit tests for MessageBox + +using System.Collections; +using Cysharp.Threading.Tasks; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace JEngine.UI.Tests +{ + [TestFixture] + public class MessageBoxTests + { + [SetUp] + public void SetUp() + { + MessageBox.Dispose(); + MessageBox.TestHandler = null; + MessageBox.SimulateNoPrefab = false; + } + + [TearDown] + public void TearDown() + { + MessageBox.Dispose(); + MessageBox.TestHandler = null; + MessageBox.SimulateNoPrefab = false; + } + + #region Static State Tests + + [Test] + public void ActiveCount_InitiallyZero() + { + Assert.AreEqual(0, MessageBox.ActiveCount); + } + + [Test] + public void PooledCount_InitiallyZero() + { + Assert.AreEqual(0, MessageBox.PooledCount); + } + + [Test] + public void Dispose_ClearsActiveAndPooledCounts() + { + MessageBox.Dispose(); + + Assert.AreEqual(0, MessageBox.ActiveCount); + Assert.AreEqual(0, MessageBox.PooledCount); + } + + [Test] + public void Dispose_CanBeCalledMultipleTimes() + { + Assert.DoesNotThrow(() => + { + MessageBox.Dispose(); + MessageBox.Dispose(); + MessageBox.Dispose(); + }); + } + + [Test] + public void CloseAll_DoesNotThrowWhenNoActiveBoxes() + { + Assert.DoesNotThrow(() => MessageBox.CloseAll()); + } + + [Test] + public void CloseAll_CanBeCalledMultipleTimes() + { + Assert.DoesNotThrow(() => + { + MessageBox.CloseAll(); + MessageBox.CloseAll(); + MessageBox.CloseAll(); + }); + } + + #endregion + + #region TestHandler Tests + + [UnityTest] + public IEnumerator Show_UsesTestHandler_WhenSet() => UniTask.ToCoroutine(async () => + { + bool handlerCalled = false; + MessageBox.TestHandler = (_, _, _, _) => + { + handlerCalled = true; + return UniTask.FromResult(true); + }; + + await MessageBox.Show("Test", "Content"); + + Assert.IsTrue(handlerCalled); + }); + + [UnityTest] + public IEnumerator Show_PassesCorrectParameters_ToTestHandler() => UniTask.ToCoroutine(async () => + { + string receivedTitle = null; + string receivedContent = null; + string receivedOk = null; + string receivedNo = null; + + MessageBox.TestHandler = (title, content, ok, no) => + { + receivedTitle = title; + receivedContent = content; + receivedOk = ok; + receivedNo = no; + return UniTask.FromResult(true); + }; + + await MessageBox.Show("MyTitle", "MyContent", "Yes", "No"); + + Assert.AreEqual("MyTitle", receivedTitle); + Assert.AreEqual("MyContent", receivedContent); + Assert.AreEqual("Yes", receivedOk); + Assert.AreEqual("No", receivedNo); + }); + + [UnityTest] + public IEnumerator Show_PassesDefaultParameters_WhenNotSpecified() => UniTask.ToCoroutine(async () => + { + string receivedOk = null; + string receivedNo = null; + + MessageBox.TestHandler = (_, _, ok, no) => + { + receivedOk = ok; + receivedNo = no; + return UniTask.FromResult(true); + }; + + await MessageBox.Show("Title", "Content"); + + Assert.AreEqual("OK", receivedOk); + Assert.AreEqual("Cancel", receivedNo); + }); + + [UnityTest] + public IEnumerator Show_ReturnsTrue_WhenTestHandlerReturnsTrue() => UniTask.ToCoroutine(async () => + { + MessageBox.TestHandler = (_, _, _, _) => UniTask.FromResult(true); + + bool result = await MessageBox.Show("Test", "Content"); + + Assert.IsTrue(result); + }); + + [UnityTest] + public IEnumerator Show_ReturnsFalse_WhenTestHandlerReturnsFalse() => UniTask.ToCoroutine(async () => + { + MessageBox.TestHandler = (_, _, _, _) => UniTask.FromResult(false); + + bool result = await MessageBox.Show("Test", "Content"); + + Assert.IsFalse(result); + }); + + [UnityTest] + public IEnumerator Show_HandlesNullParameters_WithTestHandler() => UniTask.ToCoroutine(async () => + { + string receivedTitle = "not-null"; + string receivedContent = "not-null"; + + MessageBox.TestHandler = (title, content, _, _) => + { + receivedTitle = title; + receivedContent = content; + return UniTask.FromResult(true); + }; + + await MessageBox.Show(null, null, null, null); + + Assert.IsNull(receivedTitle); + Assert.IsNull(receivedContent); + }); + + [UnityTest] + public IEnumerator Show_MultipleCalls_AllUseTestHandler() => UniTask.ToCoroutine(async () => + { + int callCount = 0; + MessageBox.TestHandler = (_, _, _, _) => + { + callCount++; + return UniTask.FromResult(true); + }; + + await MessageBox.Show("Test1", "Content1"); + await MessageBox.Show("Test2", "Content2"); + await MessageBox.Show("Test3", "Content3"); + + Assert.AreEqual(3, callCount); + }); + + [UnityTest] + public IEnumerator Show_DoesNotAffectActiveCount_WhenUsingTestHandler() => UniTask.ToCoroutine(async () => + { + MessageBox.TestHandler = (_, _, _, _) => UniTask.FromResult(true); + + int initialCount = MessageBox.ActiveCount; + await MessageBox.Show("Test", "Content"); + + Assert.AreEqual(initialCount, MessageBox.ActiveCount); + }); + + [UnityTest] + public IEnumerator Show_DoesNotAffectPooledCount_WhenUsingTestHandler() => UniTask.ToCoroutine(async () => + { + MessageBox.TestHandler = (_, _, _, _) => UniTask.FromResult(true); + + int initialCount = MessageBox.PooledCount; + await MessageBox.Show("Test", "Content"); + + Assert.AreEqual(initialCount, MessageBox.PooledCount); + }); + + #endregion + + #region No Prefab Tests (using SimulateNoPrefab) + + [UnityTest] + public IEnumerator Show_ReturnsFalse_WhenNoPrefab() => UniTask.ToCoroutine(async () => + { + MessageBox.SimulateNoPrefab = true; + LogAssert.Expect(LogType.Error, "Cannot show MessageBox: Prefab is null"); + + bool result = await MessageBox.Show("Test", "Content"); + + Assert.IsFalse(result); + }); + + [UnityTest] + public IEnumerator Show_LogsError_WhenNoPrefab() => UniTask.ToCoroutine(async () => + { + MessageBox.SimulateNoPrefab = true; + LogAssert.Expect(LogType.Error, "Cannot show MessageBox: Prefab is null"); + + await MessageBox.Show("Test", "Content"); + }); + + [UnityTest] + public IEnumerator Show_DoesNotAffectActiveCount_WhenNoPrefab() => UniTask.ToCoroutine(async () => + { + MessageBox.SimulateNoPrefab = true; + LogAssert.Expect(LogType.Error, "Cannot show MessageBox: Prefab is null"); + + int initialCount = MessageBox.ActiveCount; + await MessageBox.Show("Test", "Content"); + + Assert.AreEqual(initialCount, MessageBox.ActiveCount); + }); + + [UnityTest] + public IEnumerator Show_MultipleCalls_AllReturnFalse_WhenNoPrefab() => UniTask.ToCoroutine(async () => + { + MessageBox.SimulateNoPrefab = true; + LogAssert.Expect(LogType.Error, "Cannot show MessageBox: Prefab is null"); + LogAssert.Expect(LogType.Error, "Cannot show MessageBox: Prefab is null"); + LogAssert.Expect(LogType.Error, "Cannot show MessageBox: Prefab is null"); + + bool result1 = await MessageBox.Show("Test1", "Content1"); + bool result2 = await MessageBox.Show("Test2", "Content2"); + bool result3 = await MessageBox.Show("Test3", "Content3"); + + Assert.IsFalse(result1); + Assert.IsFalse(result2); + Assert.IsFalse(result3); + }); + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs.meta new file mode 100644 index 00000000..10c24c6f --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/MessageBoxTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6376d41dfa2d24259a9b4eacffb630c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json new file mode 100644 index 00000000..4a2cc38f --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json @@ -0,0 +1,23 @@ +{ + "name": "com.jasonxudeveloper.jengine.ui", + "version": "1.0.0", + "displayName": "JEngine.UI", + "description": "UI utilities for JEngine framework.", + "license": "MIT", + "unity": "2022.3", + "documentationUrl": "https://jengine.xgamedev.net/", + "changelogUrl": "https://github.com/JasonXuDeveloper/JEngine/blob/master/CHANGE.md", + "keywords": [ + "UI", + "JEngine", + "Framework" + ], + "author": { + "name": "Jason Xu", + "email": "jason@xgamedev.com", + "url": "https://github.com/JasonXuDeveloper" + }, + "dependencies": { + "com.cysharp.unitask": "2.5.10" + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json.meta new file mode 100644 index 00000000..9d049527 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 15839d6d08f984e9eb63e93c98ca9b0e +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/packages-lock.json b/UnityProject/Packages/packages-lock.json index 5f143141..7d844281 100644 --- a/UnityProject/Packages/packages-lock.json +++ b/UnityProject/Packages/packages-lock.json @@ -37,6 +37,14 @@ "com.tuyoogame.yooasset": "2.3.16" } }, + "com.jasonxudeveloper.jengine.ui": { + "version": "file:com.jasonxudeveloper.jengine.ui", + "depth": 0, + "source": "embedded", + "dependencies": { + "com.cysharp.unitask": "2.5.10" + } + }, "com.jasonxudeveloper.jengine.util": { "version": "file:com.jasonxudeveloper.jengine.util", "depth": 0, From 370eeb659750ed81de0c856b5059aa5510f252b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Wed, 28 Jan 2026 15:09:19 +1100 Subject: [PATCH 02/15] fix(core): address PR review feedback for Prompt abstraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make ShowDialogAsync thread-safe with volatile backing field and Interlocked.Exchange - Improve XML documentation for Prompt class with usage examples - Change default behavior to log error and return false (safer default) - Add namespace to PromptInitializer (JEngine) - Add TextMeshPro dependency to jengine.ui package.json - Remove JEngine.UI dependency from HotUpdate.Code (use Prompt abstraction instead) - Update EntryPoint.cs to use Prompt.ShowDialogAsync instead of MessageBox.Show Co-Authored-By: Claude Opus 4.5 Signed-off-by: JasonXuDeveloper - 傑 --- .../Assets/HotUpdate/Code/EntryPoint.cs | 3 +- .../HotUpdate/Code/HotUpdate.Code.asmdef | 3 +- .../Assets/Scripts/PromptInitializer.cs | 25 ++++++----- .../Runtime/Prompt.cs | 43 +++++++++++++++---- .../package.json | 3 +- 5 files changed, 53 insertions(+), 24 deletions(-) diff --git a/UnityProject/Assets/HotUpdate/Code/EntryPoint.cs b/UnityProject/Assets/HotUpdate/Code/EntryPoint.cs index ef69814a..3cb7b400 100644 --- a/UnityProject/Assets/HotUpdate/Code/EntryPoint.cs +++ b/UnityProject/Assets/HotUpdate/Code/EntryPoint.cs @@ -27,7 +27,6 @@ using JEngine.Core; using JEngine.Core.Encrypt; using JEngine.Core.Update; -using JEngine.UI; using Obfuz; using UnityEngine; using UnityEngine.Scripting; @@ -64,7 +63,7 @@ async UniTask LoadAddOnPackage() { OnStatusUpdate = static status => Debug.Log($"[AddOn1] Status: {GetStatusText(status)}"), OnVersionUpdate = static version => Debug.Log($"[AddOn1] Version: {version}"), - OnDownloadPrompt = static (count, size) => MessageBox.Show("Notice", + OnDownloadPrompt = static (count, size) => Prompt.ShowDialogAsync("Notice", $"[AddOn1] Need to download {count} files, total size {size / 1024f / 1024f:F2}MB. Continue?", "Yes", "No"), OnDownloadProgress = static data => diff --git a/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef b/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef index 02fd9a1d..2733e83e 100644 --- a/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef +++ b/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef @@ -12,8 +12,7 @@ "GUID:3fe1a3e70da50184f9897101cad7e4f2", "GUID:6055be8ebefd69e48b49212b09b47b2f", "GUID:ba02d1bbd77cf4a0c8606d3e5cbbbe79", - "GUID:5c8e1f4d7a3b9e2c6f0d8a4b7e3c1f9d", - "GUID:5655bcbaa4dec434b86ecd07e2c6f24d" + "GUID:5c8e1f4d7a3b9e2c6f0d8a4b7e3c1f9d" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/UnityProject/Assets/Scripts/PromptInitializer.cs b/UnityProject/Assets/Scripts/PromptInitializer.cs index f55349db..166a31e8 100644 --- a/UnityProject/Assets/Scripts/PromptInitializer.cs +++ b/UnityProject/Assets/Scripts/PromptInitializer.cs @@ -34,7 +34,7 @@ // "com.jasonxudeveloper.jengine.ui": "1.0.0" // // If you don't need MessageBox dialogs, you can: -// - Delete this script entirely (Bootstrap will log warnings and continue) +// - Delete this script entirely (Bootstrap will log errors and continue) // - Implement your own custom dialog provider by assigning to Prompt.ShowDialogAsync // ============================================================================ @@ -42,17 +42,20 @@ using JEngine.UI; using UnityEngine; -/// -/// Initializes the Prompt system to use MessageBox for dialogs. -/// This runs automatically before any scene loads. -/// -public static class PromptInitializer +namespace JEngine { - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - private static void Initialize() + /// + /// Initializes the Prompt system to use MessageBox for dialogs. + /// This runs automatically before any scene loads. + /// + public static class PromptInitializer { - // Register MessageBox as the dialog provider for JEngine.Core - Prompt.ShowDialogAsync = MessageBox.Show; - Debug.Log("[JEngine] Prompt system initialized with MessageBox provider."); + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Initialize() + { + // Register MessageBox as the dialog provider for JEngine.Core + Prompt.ShowDialogAsync = MessageBox.Show; + Debug.Log("[JEngine] Prompt system initialized with MessageBox provider."); + } } } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs index a4868c96..5f9bdff3 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs @@ -24,28 +24,55 @@ // THE SOFTWARE. using System; +using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; namespace JEngine.Core { /// - /// User prompt/dialog abstraction. Configure ShowDialogAsync to customize behavior. + /// Provides an abstraction layer for displaying user prompts and dialogs. + /// This allows the core framework to request user input without depending on specific UI implementations. /// + /// + /// + /// By default, dialogs log an error and return false. To enable actual dialogs, + /// assign a handler to before Bootstrap runs. + /// + /// + /// Example using JEngine.UI.MessageBox: + /// + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + /// static void Init() => Prompt.ShowDialogAsync = MessageBox.Show; + /// + /// + /// public static class Prompt { + private static volatile Func> _showDialogAsync = DefaultShowDialog; + /// - /// Shows a dialog to the user and returns the result. - /// Parameters: title, content, okText (null to hide), noText (null to hide) - /// Returns: true if OK clicked, false if No/Cancel clicked - /// Default: logs warning and returns true (continues execution). + /// Gets or sets the dialog handler function. /// - public static Func> ShowDialogAsync = DefaultShowDialog; + /// + /// A function that displays a dialog to the user. + /// Parameters: title, content, okText (null to hide OK button), noText (null to hide No button). + /// Returns: true if OK clicked, false if No/Cancel clicked. + /// + /// + /// This property is thread-safe. Assign before Bootstrap runs using + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]. + /// + public static Func> ShowDialogAsync + { + get => _showDialogAsync; + set => Interlocked.Exchange(ref _showDialogAsync, value ?? DefaultShowDialog); + } private static UniTask DefaultShowDialog(string title, string content, string ok, string no) { - Debug.LogWarning($"[JEngine] Dialog provider not configured. {title}: {content}"); - return UniTask.FromResult(true); + Debug.LogError($"[JEngine] Dialog provider not configured. Cannot display: {title}: {content}"); + return UniTask.FromResult(false); } } } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json index 4a2cc38f..54247621 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json @@ -18,6 +18,7 @@ "url": "https://github.com/JasonXuDeveloper" }, "dependencies": { - "com.cysharp.unitask": "2.5.10" + "com.cysharp.unitask": "2.5.10", + "com.unity.textmeshpro": "3.0.6" } } From b3b113929351bb81ba19b123470f32a983f59d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Wed, 28 Jan 2026 15:14:55 +1100 Subject: [PATCH 03/15] fix(core): remove redundant initialization and namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use null-coalescing in getter instead of field initializer - Remove namespace from PromptInitializer (user-level code) Co-Authored-By: Claude Opus 4.5 Signed-off-by: JasonXuDeveloper - 傑 --- .../Assets/Scripts/PromptInitializer.cs | 23 ++++++++----------- .../Runtime/Prompt.cs | 4 ++-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/UnityProject/Assets/Scripts/PromptInitializer.cs b/UnityProject/Assets/Scripts/PromptInitializer.cs index 166a31e8..e6c539ee 100644 --- a/UnityProject/Assets/Scripts/PromptInitializer.cs +++ b/UnityProject/Assets/Scripts/PromptInitializer.cs @@ -42,20 +42,17 @@ using JEngine.UI; using UnityEngine; -namespace JEngine +/// +/// Initializes the Prompt system to use MessageBox for dialogs. +/// This runs automatically before any scene loads. +/// +public static class PromptInitializer { - /// - /// Initializes the Prompt system to use MessageBox for dialogs. - /// This runs automatically before any scene loads. - /// - public static class PromptInitializer + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Initialize() { - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - private static void Initialize() - { - // Register MessageBox as the dialog provider for JEngine.Core - Prompt.ShowDialogAsync = MessageBox.Show; - Debug.Log("[JEngine] Prompt system initialized with MessageBox provider."); - } + // Register MessageBox as the dialog provider for JEngine.Core + Prompt.ShowDialogAsync = MessageBox.Show; + Debug.Log("[JEngine] Prompt system initialized with MessageBox provider."); } } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs index 5f9bdff3..4804d97e 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.core/Runtime/Prompt.cs @@ -49,7 +49,7 @@ namespace JEngine.Core /// public static class Prompt { - private static volatile Func> _showDialogAsync = DefaultShowDialog; + private static volatile Func> _showDialogAsync; /// /// Gets or sets the dialog handler function. @@ -65,7 +65,7 @@ public static class Prompt /// public static Func> ShowDialogAsync { - get => _showDialogAsync; + get => _showDialogAsync ?? DefaultShowDialog; set => Interlocked.Exchange(ref _showDialogAsync, value ?? DefaultShowDialog); } From f9648254be4d574a884107db2d2be8a693cf4739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Wed, 28 Jan 2026 15:17:10 +1100 Subject: [PATCH 04/15] fix(core): remove JEngine.Util reference from HotUpdate.Code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HotUpdate code should not directly depend on utility packages. Co-Authored-By: Claude Opus 4.5 Signed-off-by: JasonXuDeveloper - 傑 --- UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef b/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef index 2733e83e..c5553745 100644 --- a/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef +++ b/UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef @@ -11,8 +11,7 @@ "GUID:e34a5702dd353724aa315fb8011f08c3", "GUID:3fe1a3e70da50184f9897101cad7e4f2", "GUID:6055be8ebefd69e48b49212b09b47b2f", - "GUID:ba02d1bbd77cf4a0c8606d3e5cbbbe79", - "GUID:5c8e1f4d7a3b9e2c6f0d8a4b7e3c1f9d" + "GUID:ba02d1bbd77cf4a0c8606d3e5cbbbe79" ], "includePlatforms": [], "excludePlatforms": [], From 169baa4dc185dbfc049c0a11b4abc0912044e447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Wed, 28 Jan 2026 15:20:49 +1100 Subject: [PATCH 05/15] chore(ui): update packages-lock.json for TextMeshPro dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 Signed-off-by: JasonXuDeveloper - 傑 --- UnityProject/Packages/packages-lock.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UnityProject/Packages/packages-lock.json b/UnityProject/Packages/packages-lock.json index 7d844281..54a63786 100644 --- a/UnityProject/Packages/packages-lock.json +++ b/UnityProject/Packages/packages-lock.json @@ -42,7 +42,8 @@ "depth": 0, "source": "embedded", "dependencies": { - "com.cysharp.unitask": "2.5.10" + "com.cysharp.unitask": "2.5.10", + "com.unity.textmeshpro": "3.0.6" } }, "com.jasonxudeveloper.jengine.util": { From 562b5c4bc15ccb52643d156e749625ced8d14a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Wed, 28 Jan 2026 15:21:13 +1100 Subject: [PATCH 06/15] chore(ui): set initial version to 0.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New package should start at 0.0.0. Co-Authored-By: Claude Opus 4.5 Signed-off-by: JasonXuDeveloper - 傑 --- .../Packages/com.jasonxudeveloper.jengine.ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json index 54247621..f1198d39 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json @@ -1,6 +1,6 @@ { "name": "com.jasonxudeveloper.jengine.ui", - "version": "1.0.0", + "version": "0.0.0", "displayName": "JEngine.UI", "description": "UI utilities for JEngine framework.", "license": "MIT", From 3c96d3fa5eacfc29be04c7f47fc195887b958c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Wed, 28 Jan 2026 15:21:56 +1100 Subject: [PATCH 07/15] docs: add guidelines for creating new JEngine packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New packages should start at version 0.0.0 - Document naming convention and required files - Add ui scope to commit message conventions Co-Authored-By: Claude Opus 4.5 Signed-off-by: JasonXuDeveloper - 傑 --- CLAUDE.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 7ea96ff3..3c333559 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,6 +160,39 @@ public static bool Flag => _flag; 3. Add menu items via `[MenuItem]` attribute 4. Handle domain reloads if maintaining state +### Creating New JEngine Packages + +When creating a new JEngine package: + +1. **Version**: Start at `0.0.0` (not `1.0.0`) +2. **Naming**: Use `com.jasonxudeveloper.jengine.` format +3. **Location**: `Packages/com.jasonxudeveloper.jengine./` +4. **Required files**: + - `package.json` - Package manifest with version `0.0.0` + - `Runtime/.asmdef` - Assembly definition + - `README.md` - Package documentation (optional) + +Example `package.json`: +```json +{ + "name": "com.jasonxudeveloper.jengine.", + "version": "0.0.0", + "displayName": "JEngine.", + "description": "Description here.", + "license": "MIT", + "unity": "2022.3", + "author": { + "name": "Jason Xu", + "email": "jason@xgamedev.com", + "url": "https://github.com/JasonXuDeveloper" + }, + "dependencies": {} +} +``` + +5. **CI Updates**: Add package path to `.github/workflows/pr-tests.yml` and release support to `.github/workflows/release.yml` +6. **Scopes**: Add new scope to commit message conventions + ## Code Review Checklist - [ ] Follows namespace conventions @@ -199,6 +232,7 @@ All commits should follow the Conventional Commits specification to enable autom - `core` - Changes to JEngine.Core package - `util` - Changes to JEngine.Util package +- `ui` - Changes to JEngine.UI package - `ci` - Changes to CI/CD workflows - `docs` - Changes to documentation From 137c791f208631864cb13fce8ec4223d8b36dbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JasonXuDeveloper=20-=20=E5=82=91?= Date: Wed, 28 Jan 2026 15:24:17 +1100 Subject: [PATCH 08/15] docs: restructure CLAUDE.md into modular rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split verbose CLAUDE.md (295 lines) into focused files: - .claude/rules/commit-conventions.md - .claude/rules/package-creation.md - .claude/rules/coding-patterns.md Main CLAUDE.md now 84 lines with essential info only. Co-Authored-By: Claude Opus 4.5 Signed-off-by: JasonXuDeveloper - 傑 --- .claude/rules/coding-patterns.md | 50 ++++++ .claude/rules/commit-conventions.md | 73 +++++++++ .claude/rules/package-creation.md | 38 +++++ CLAUDE.md | 239 ++-------------------------- 4 files changed, 175 insertions(+), 225 deletions(-) create mode 100644 .claude/rules/coding-patterns.md create mode 100644 .claude/rules/commit-conventions.md create mode 100644 .claude/rules/package-creation.md diff --git a/.claude/rules/coding-patterns.md b/.claude/rules/coding-patterns.md new file mode 100644 index 00000000..4e1cccfe --- /dev/null +++ b/.claude/rules/coding-patterns.md @@ -0,0 +1,50 @@ +# JEngine Coding Patterns + +## Async Patterns + +Use `UniTask` for async operations, not `System.Threading.Tasks.Task`: +```csharp +public async UniTask LoadAssetAsync() { } +``` + +## Thread Safety + +For properties accessed across callbacks: +```csharp +private static volatile bool _flag; +public static bool Flag => _flag; +``` + +## Encryption Architecture + +JEngine supports three encryption algorithms: +- **XOR** (`EncryptionOption.Xor`) - Fast, simple +- **AES** (`EncryptionOption.Aes`) - Moderate security +- **ChaCha20** (`EncryptionOption.ChaCha20`) - High security + +Each has implementations for bundles and manifests in `Runtime/Encrypt/`. + +### Adding New Encryption + +1. Create config class in `Runtime/Encrypt/Config/` +2. Implement bundle encryption in `Runtime/Encrypt/Bundle/` +3. Implement manifest encryption in `Runtime/Encrypt/Manifest/` +4. Add to `EncryptionOption` enum +5. Update `EncryptionMapping` class + +## ScriptableObject Configuration + +Use `ScriptableObject` for runtime configuration: +```csharp +public abstract class ConfigBase : ScriptableObject where T : ConfigBase +{ + public static T Instance { get; } +} +``` + +## Editor Scripts + +- Use `[InitializeOnLoad]` for editor initialization +- Handle Unity domain reloads properly (state resets on recompile) +- Use `SessionState` or `EditorPrefs` for persistent editor state +- Clean up resources in `EditorApplication.quitting` diff --git a/.claude/rules/commit-conventions.md b/.claude/rules/commit-conventions.md new file mode 100644 index 00000000..521a5a5e --- /dev/null +++ b/.claude/rules/commit-conventions.md @@ -0,0 +1,73 @@ +# Commit Message Format + +All commits should follow the Conventional Commits specification to enable automatic changelog generation. + +## Format + +``` +(): + + + +