├── .gitignore ├── Unity-Package ├── Assets │ └── root │ │ ├── Runtime │ │ ├── .gitignore │ │ └── YOUR_PACKAGE_ID.Runtime.asmdef │ │ ├── Samples~ │ │ └── .gitignore │ │ ├── Tests │ │ ├── Editor │ │ │ ├── .gitignore │ │ │ └── YOUR_PACKAGE_ID.Editor.Tests.asmdef │ │ └── Runtime │ │ │ ├── .gitignore │ │ │ └── YOUR_PACKAGE_ID.Tests.asmdef │ │ ├── Documentation~ │ │ └── .gitignore │ │ ├── Editor │ │ ├── Gizmos │ │ │ ├── .gitignore │ │ │ └── icon.png │ │ └── Scripts │ │ │ ├── .gitignore │ │ │ └── YOUR_PACKAGE_ID.Editor.asmdef │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ └── LICENSE └── .gitignore ├── .vscode └── settings.json ├── Installer ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── Assets │ └── YOUR_PACKAGE_TITLE │ │ ├── YOUR_PACKAGE_ID.Installer.asmdef │ │ ├── Tests │ │ ├── YOUR_PACKAGE_ID.Installer.Tests.asmdef │ │ ├── Files │ │ │ ├── scopedregistries_gone.json │ │ │ ├── scopedregistries_empty_1.json │ │ │ ├── scopedregistries_empty_2.json │ │ │ ├── scopes_gone.json │ │ │ ├── scopes_empty.json │ │ │ ├── scopes_partial_5.json │ │ │ ├── scopes_partial_4.json │ │ │ ├── scopes_partial_3.json │ │ │ ├── scopes_partial_2.json │ │ │ ├── scopes_partial_1.json │ │ │ └── Correct │ │ │ │ └── correct_manifest.json │ │ ├── ManifestInstallerTests.cs │ │ └── VersionComparisonTests.cs │ │ ├── Installer.cs │ │ ├── PackageExporter.cs │ │ ├── README.md │ │ ├── Installer.Manifest.cs │ │ └── SimpleJSON.cs └── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── test_pull_request.yml-sample │ ├── test_unity_plugin.yml │ └── release.yml-sample ├── LICENSE ├── docs ├── Manual-Package-Rename.md ├── Deploy-GitHub.md ├── Deploy-OpenUPM.md └── Deploy-npmjs.md ├── commands ├── init.ps1 └── bump-version.ps1 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Claude 2 | .claude -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Runtime/.gitignore: -------------------------------------------------------------------------------- 1 | # Except this file 2 | !.gitignore -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Samples~/.gitignore: -------------------------------------------------------------------------------- 1 | # Except this file 2 | !.gitignore -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Tests/Editor/.gitignore: -------------------------------------------------------------------------------- 1 | # Except this file 2 | !.gitignore -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Documentation~/.gitignore: -------------------------------------------------------------------------------- 1 | # Except this file 2 | !.gitignore -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Editor/Gizmos/.gitignore: -------------------------------------------------------------------------------- 1 | # Except this file 2 | !.gitignore -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Editor/Scripts/.gitignore: -------------------------------------------------------------------------------- 1 | # Except this file 2 | !.gitignore -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Tests/Runtime/.gitignore: -------------------------------------------------------------------------------- 1 | # Except this file 2 | !.gitignore -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "editmode", 4 | "playmode" 5 | ] 6 | } -------------------------------------------------------------------------------- /Installer/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "visualstudiotoolsforunity.vstuc" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Editor/Gizmos/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IvanMurzak/Unity-Package-Template/HEAD/Unity-Package/Assets/root/Editor/Gizmos/icon.png -------------------------------------------------------------------------------- /Installer/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Unity", 6 | "type": "vstuc", 7 | "request": "attach" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /Unity-Package/Assets/root/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.1] - 2025-03-29 4 | ### Fixed 5 | - Resolved a bug causing crashes when initializing the package on Unity 2021.3. 6 | - Fixed incorrect behavior in the custom editor window. 7 | 8 | ## [1.0.0] - 2025-03-15 9 | ### Added 10 | - Initial release of the Unity package. 11 | - Added core functionality for A, B, and C. 12 | - Included documentation and example scenes. -------------------------------------------------------------------------------- /Unity-Package/Assets/root/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YOUR_PACKAGE_ID_LOWERCASE", 3 | "displayName": "YOUR_PACKAGE_NAME", 4 | "author": { 5 | "name": "Your_Name", 6 | "url": "https://github.com/Your_Name" 7 | }, 8 | "keywords": [ 9 | "keyword1", 10 | "keyword2" 11 | ], 12 | "version": "1.0.0", 13 | "unity": "2019.4", 14 | "description": "Some description", 15 | "dependencies": {} 16 | } -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Runtime/YOUR_PACKAGE_ID.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YOUR_PACKAGE_ID.Runtime", 3 | "references": [], 4 | "includePlatforms": [], 5 | "excludePlatforms": [ 6 | "Editor" 7 | ], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [], 11 | "autoReferenced": true, 12 | "defineConstraints": [], 13 | "versionDefines": [], 14 | "noEngineReferences": false 15 | } -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Editor/Scripts/YOUR_PACKAGE_ID.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YOUR_PACKAGE_ID.Editor", 3 | "references": [ 4 | "YOUR_PACKAGE_ID.Runtime" 5 | ], 6 | "includePlatforms": [], 7 | "excludePlatforms": [], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [], 11 | "autoReferenced": true, 12 | "defineConstraints": [], 13 | "versionDefines": [], 14 | "noEngineReferences": false 15 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/YOUR_PACKAGE_ID.Installer.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YOUR_PACKAGE_ID.Installer", 3 | "rootNamespace": "YOUR_PACKAGE_ID.Installer", 4 | "references": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": false, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Tests/Runtime/YOUR_PACKAGE_ID.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YOUR_PACKAGE_ID.Tests", 3 | "references": [ 4 | "UnityEngine.TestRunner", 5 | "YOUR_PACKAGE_ID.Runtime" 6 | ], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": true, 11 | "precompiledReferences": [ 12 | "nunit.framework.dll" 13 | ], 14 | "autoReferenced": false, 15 | "defineConstraints": [ 16 | "UNITY_INCLUDE_TESTS" 17 | ], 18 | "versionDefines": [], 19 | "noEngineReferences": false 20 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/YOUR_PACKAGE_ID.Installer.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YOUR_PACKAGE_ID.Installer.Tests", 3 | "rootNamespace": "YOUR_PACKAGE_ID.Installer.Tests", 4 | "references": [ 5 | "YOUR_PACKAGE_ID.Installer" 6 | ], 7 | "includePlatforms": [ 8 | "Editor" 9 | ], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": true, 13 | "precompiledReferences": [ 14 | "nunit.framework.dll" 15 | ], 16 | "autoReferenced": false, 17 | "defineConstraints": [ 18 | "UNITY_INCLUDE_TESTS" 19 | ], 20 | "versionDefines": [], 21 | "noEngineReferences": false 22 | } -------------------------------------------------------------------------------- /Unity-Package/Assets/root/Tests/Editor/YOUR_PACKAGE_ID.Editor.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YOUR_PACKAGE_ID.Editor.Tests", 3 | "references": [ 4 | "UnityEngine.TestRunner", 5 | "UnityEditor.TestRunner", 6 | "YOUR_PACKAGE_ID.Runtime", 7 | "YOUR_PACKAGE_ID.Editor" 8 | ], 9 | "includePlatforms": [ 10 | "Editor" 11 | ], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": false, 14 | "overrideReferences": true, 15 | "precompiledReferences": [ 16 | "nunit.framework.dll" 17 | ], 18 | "autoReferenced": false, 19 | "defineConstraints": [ 20 | "UNITY_INCLUDE_TESTS" 21 | ], 22 | "versionDefines": [], 23 | "noEngineReferences": false 24 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: IvanMurzak 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_gone.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | } 19 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_empty_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [] 20 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopedregistries_empty_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [] 20 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Installer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | ┌────────────────────────────────────────────────────────────────────────────┐ 3 | │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ 4 | │ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ 5 | │ Copyright (c) 2025 Ivan Murzak │ 6 | │ Licensed under the MIT License. │ 7 | │ See the LICENSE file in the project root for more information. │ 8 | └────────────────────────────────────────────────────────────────────────────┘ 9 | */ 10 | #nullable enable 11 | using UnityEditor; 12 | 13 | namespace YOUR_PACKAGE_ID.Installer 14 | { 15 | [InitializeOnLoad] 16 | public static partial class Installer 17 | { 18 | public const string PackageId = "YOUR_PACKAGE_ID_LOWERCASE"; 19 | public const string Version = "1.0.0"; 20 | 21 | static Installer() 22 | { 23 | #if !IVAN_MURZAK_INSTALLER_PROJECT 24 | AddScopedRegistryIfNeeded(ManifestPath); 25 | #endif 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_gone.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [ 20 | { 21 | "name": "package.openupm.com", 22 | "url": "https://package.openupm.com" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ivan Murzak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [ 20 | { 21 | "name": "package.openupm.com", 22 | "url": "https://package.openupm.com", 23 | "scopes": [] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Unity-Package/Assets/root/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ivan Murzak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/Manual-Package-Rename.md: -------------------------------------------------------------------------------- 1 | # Manual `Package` rename 2 | 3 | #### 1️⃣ Customize `Assets/root/package.json` 4 | 5 | - 👉 **Update** `name` 6 | > Sample: `com.github.your_name.package` 7 | > Instead of the word `package` use a word or couple of words that explains the main purpose of the package. 8 | 9 | - 👉 **Update** `unity` to setup minimum supported Unity version 10 | - 👉 **Update** `displayName`, `version`, `description`, `author`, `keywords` to your needs 11 | 12 | #### 2️⃣ Do you need Tests? 13 | 14 |
15 | ❌ NO - click 16 | 17 | - 👉 **Delete** `Assets/root/Tests` folder 18 | - 👉 **Delete** `.github/workflows` folder 19 | 20 |
21 | 22 |
23 | ✅ YES - click 24 | 25 | - 👉 **Repeat** these actions for these files. 26 | 27 | - Update the files: 28 | - `Assets/root/Tests/Base/Package.Editor.Tests.asmdef` 29 | - `Assets/root/Tests/Base/Package.Tests.asmdef` 30 | 31 | - Apply these actions to files above: 32 | - 👉 **Rename** the `Package` part of the file name 33 | - 👉 **Replace** the `Package` keyword in the file content (multiple places) 34 | 35 |
36 | -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_5.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [ 20 | { 21 | "name": "package.openupm.com", 22 | "url": "https://package.openupm.com", 23 | "scopes": [ 24 | "com.ivanmurzak" 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_4.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [ 20 | { 21 | "name": "package.openupm.com", 22 | "url": "https://package.openupm.com", 23 | "scopes": [ 24 | "com.ivanmurzak", 25 | "extensions.unity" 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [ 20 | { 21 | "name": "package.openupm.com", 22 | "url": "https://package.openupm.com", 23 | "scopes": [ 24 | "com.ivanmurzak", 25 | "extensions.unity", 26 | "org.nuget.com.ivanmurzak" 27 | ] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [ 20 | { 21 | "name": "package.openupm.com", 22 | "url": "https://package.openupm.com", 23 | "scopes": [ 24 | "com.ivanmurzak", 25 | "extensions.unity", 26 | "org.nuget.com.ivanmurzak", 27 | "org.nuget.microsoft" 28 | ] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/scopes_partial_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [ 20 | { 21 | "name": "package.openupm.com", 22 | "url": "https://package.openupm.com", 23 | "scopes": [ 24 | "com.ivanmurzak", 25 | "extensions.unity", 26 | "org.nuget.com.ivanmurzak", 27 | "org.nuget.microsoft", 28 | "org.nuget.system" 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/Files/Correct/correct_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "com.unity.ide.visualstudio": "2.0.23", 4 | "com.unity.test-framework": "1.1.33", 5 | "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", 6 | "org.nuget.microsoft.aspnetcore.signalr.client": "9.0.7", 7 | "org.nuget.microsoft.aspnetcore.signalr.protocols.json": "9.0.7", 8 | "org.nuget.microsoft.bcl.memory": "9.0.7", 9 | "org.nuget.microsoft.codeanalysis.csharp": "4.13.0", 10 | "org.nuget.microsoft.extensions.caching.abstractions": "9.0.7", 11 | "org.nuget.microsoft.extensions.dependencyinjection.abstractions": "9.0.7", 12 | "org.nuget.microsoft.extensions.hosting": "9.0.7", 13 | "org.nuget.microsoft.extensions.hosting.abstractions": "9.0.7", 14 | "org.nuget.microsoft.extensions.logging.abstractions": "9.0.7", 15 | "org.nuget.r3": "1.3.0", 16 | "org.nuget.system.text.json": "9.0.7", 17 | "PACKAGE_ID": "PACKAGE_VERSION" 18 | }, 19 | "scopedRegistries": [ 20 | { 21 | "name": "package.openupm.com", 22 | "url": "https://package.openupm.com", 23 | "scopes": [ 24 | "com.ivanmurzak", 25 | "extensions.unity", 26 | "org.nuget.com.ivanmurzak", 27 | "org.nuget.microsoft", 28 | "org.nuget.system", 29 | "org.nuget.r3" 30 | ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/PackageExporter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | ┌────────────────────────────────────────────────────────────────────────────┐ 3 | │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ 4 | │ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ 5 | │ Copyright (c) 2025 Ivan Murzak │ 6 | │ Licensed under the MIT License. │ 7 | │ See the LICENSE file in the project root for more information. │ 8 | └────────────────────────────────────────────────────────────────────────────┘ 9 | */ 10 | #nullable enable 11 | using UnityEngine; 12 | using UnityEditor; 13 | using System.IO; 14 | 15 | namespace YOUR_PACKAGE_ID.Installer 16 | { 17 | public static class PackageExporter 18 | { 19 | public static void ExportPackage() 20 | { 21 | var packagePath = "Assets/YOUR_PACKAGE_NAME_INSTALLER"; 22 | var outputPath = "build/YOUR_PACKAGE_NAME_INSTALLER_FILE.unitypackage"; 23 | 24 | // Ensure build directory exists 25 | var buildDir = Path.GetDirectoryName(outputPath); 26 | if (!Directory.Exists(buildDir)) 27 | { 28 | Directory.CreateDirectory(buildDir); 29 | } 30 | 31 | // Export the package 32 | AssetDatabase.ExportPackage(packagePath, outputPath, ExportPackageOptions.Recurse); 33 | 34 | Debug.Log($"Package exported to: {outputPath}"); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Installer/.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/main/Docs/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Mm]emoryCaptures/ 12 | 13 | # Asset meta data should only be ignored when the corresponding asset is also ignored 14 | !/[Aa]ssets/**/*.meta 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # Autogenerated Jetbrains Rider plugin 20 | [Aa]ssets/Plugins/Editor/JetBrains* 21 | 22 | # Visual Studio cache directory 23 | .vs/ 24 | 25 | # Gradle cache directory 26 | .gradle/ 27 | 28 | # Autogenerated VS/MD/Consulo solution and project files 29 | ExportedObj/ 30 | .consulo/ 31 | *.csproj 32 | *.unityproj 33 | *.sln 34 | *.suo 35 | *.tmp 36 | *.user 37 | *.userprefs 38 | *.pidb 39 | *.booproj 40 | *.svd 41 | *.pdb 42 | *.mdb 43 | *.opendb 44 | *.VC.db 45 | 46 | # Unity3D generated meta files 47 | *.pidb.meta 48 | *.pdb.meta 49 | *.mdb.meta 50 | 51 | # Unity3D generated file on crash reports 52 | sysinfo.txt 53 | 54 | # Builds 55 | *.apk 56 | *.unitypackage 57 | 58 | # Crashlytics generated file 59 | crashlytics-build.properties 60 | 61 | */AndroidLogcatSettings.asset 62 | /Assets/StreamingAssets 63 | 64 | # Crashlytics generated file 65 | crashlytics-build.properties 66 | 67 | # ODIN Ignore the auto-generated AOT compatibility dll. 68 | /Assets/Plugins/Sirenix/Assemblies/AOT/* 69 | /Assets/Plugins/Sirenix/Assemblies/AOT** 70 | 71 | # ODIN Ignore all unpacked demos. 72 | /Assets/Plugins/Sirenix/Demos/* 73 | 74 | # ODIN plugin 75 | /Assets/Plugins/Sirenix** 76 | 77 | # Claude 78 | .claude -------------------------------------------------------------------------------- /Unity-Package/.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/main/Docs/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Mm]emoryCaptures/ 12 | 13 | # Asset meta data should only be ignored when the corresponding asset is also ignored 14 | !/[Aa]ssets/**/*.meta 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # Autogenerated Jetbrains Rider plugin 20 | [Aa]ssets/Plugins/Editor/JetBrains* 21 | 22 | # Visual Studio cache directory 23 | .vs/ 24 | 25 | # Gradle cache directory 26 | .gradle/ 27 | 28 | # Autogenerated VS/MD/Consulo solution and project files 29 | ExportedObj/ 30 | .consulo/ 31 | *.csproj 32 | *.unityproj 33 | *.sln 34 | *.suo 35 | *.tmp 36 | *.user 37 | *.userprefs 38 | *.pidb 39 | *.booproj 40 | *.svd 41 | *.pdb 42 | *.mdb 43 | *.opendb 44 | *.VC.db 45 | 46 | # Unity3D generated meta files 47 | *.pidb.meta 48 | *.pdb.meta 49 | *.mdb.meta 50 | 51 | # Unity3D generated file on crash reports 52 | sysinfo.txt 53 | 54 | # Builds 55 | *.apk 56 | *.unitypackage 57 | 58 | # Crashlytics generated file 59 | crashlytics-build.properties 60 | 61 | */AndroidLogcatSettings.asset 62 | /Assets/StreamingAssets 63 | 64 | # Crashlytics generated file 65 | crashlytics-build.properties 66 | 67 | # ODIN Ignore the auto-generated AOT compatibility dll. 68 | /Assets/Plugins/Sirenix/Assemblies/AOT/* 69 | /Assets/Plugins/Sirenix/Assemblies/AOT** 70 | 71 | # ODIN Ignore all unpacked demos. 72 | /Assets/Plugins/Sirenix/Demos/* 73 | 74 | # ODIN plugin 75 | /Assets/Plugins/Sirenix** 76 | 77 | # Claude 78 | .claude -------------------------------------------------------------------------------- /Installer/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.DS_Store": true, 4 | "**/.git": true, 5 | "**/.vs": true, 6 | "**/.gitmodules": true, 7 | "**/.vsconfig": true, 8 | "**/*.booproj": true, 9 | "**/*.pidb": true, 10 | "**/*.suo": true, 11 | "**/*.user": true, 12 | "**/*.userprefs": true, 13 | "**/*.unityproj": true, 14 | "**/*.dll": true, 15 | "**/*.exe": true, 16 | "**/*.pdf": true, 17 | "**/*.mid": true, 18 | "**/*.midi": true, 19 | "**/*.wav": true, 20 | "**/*.gif": true, 21 | "**/*.ico": true, 22 | "**/*.jpg": true, 23 | "**/*.jpeg": true, 24 | "**/*.png": true, 25 | "**/*.psd": true, 26 | "**/*.tga": true, 27 | "**/*.tif": true, 28 | "**/*.tiff": true, 29 | "**/*.3ds": true, 30 | "**/*.3DS": true, 31 | "**/*.fbx": true, 32 | "**/*.FBX": true, 33 | "**/*.lxo": true, 34 | "**/*.LXO": true, 35 | "**/*.ma": true, 36 | "**/*.MA": true, 37 | "**/*.obj": true, 38 | "**/*.OBJ": true, 39 | "**/*.asset": true, 40 | "**/*.cubemap": true, 41 | "**/*.flare": true, 42 | "**/*.mat": true, 43 | "**/*.meta": true, 44 | "**/*.prefab": true, 45 | "**/*.unity": true, 46 | "build/": true, 47 | "Build/": true, 48 | "Library/": true, 49 | "library/": true, 50 | "obj/": true, 51 | "Obj/": true, 52 | "Logs/": true, 53 | "logs/": true, 54 | "ProjectSettings/": true, 55 | "UserSettings/": true, 56 | "temp/": true, 57 | "Temp/": true 58 | }, 59 | "files.associations": { 60 | "*.asset": "yaml", 61 | "*.meta": "yaml", 62 | "*.prefab": "yaml", 63 | "*.unity": "yaml", 64 | }, 65 | "explorer.fileNesting.enabled": true, 66 | "explorer.fileNesting.patterns": { 67 | "*.sln": "*.csproj", 68 | }, 69 | "dotnet.defaultSolution": "Installer.sln", 70 | "cSpell.words": [ 71 | "ivanmurzak" 72 | ] 73 | } -------------------------------------------------------------------------------- /docs/Deploy-GitHub.md: -------------------------------------------------------------------------------- 1 | # Deploy your package using `GitHub` 2 | 3 | GitHub public repository could be used as a host for your package and directly imported into Unity project from GitHub repository. 4 | 5 | > ⚠️ GitHub distribution method doesn't support automatic dependency resolving. Recommended to deploy package to OpenUPM if your package has dependencies. Also, you would need to use only OpenUPM-CLI to resolve dependencies automatically. 6 | 7 | ## Deploy 8 | 9 | 1. Increment package version in the file `Assets/root/package.json`. It has `version` property. 10 | > Any further updates should be done by incrementing package version and making another GitHub release. 11 | 2. Create GitHub Release 12 | 1. Go to your GitHub repository 13 | 2. Click on `Releases` 14 | 3. Click on `Draft a new release` 15 | 4. Use `tag` that is equals to your package version name 16 | 17 | # Installation 18 | 19 | ### Option 1: Using Package Manager (recommended) 20 | 21 | - Open `Package Manager` in Unity Editor 22 | - Click on the small `+` button in the top left corner 23 | - Select `Add package from git URL` 24 | - Paste URL to your GitHub repository with simple modification: 25 | `https://github.com/USER/REPO.git` 26 | Don't forget to replace **USER** and **REPO** to yours 27 | 28 | #### **Or** you may use special version if you made one 29 | 30 | > To make a version at GitHub, you may need to create Tag with the version name. Also, I would recommend to make a GitHub Release as well. 31 | 32 | `https://github.com/USER/REPO.git#v1.0.0` 33 | Don't forget to replace **USER** and **REPO** to yours 34 | 35 | ### Option 2: Manual 36 | 37 | Modify `manifest.json` file. Need to add the line into your's `dependencies` object. Change `your.package.name` to the name of your package. And replace **USER** and **REPO** to yours. 38 | 39 | ```json 40 | { 41 | "dependencies": { 42 | "your.package.name": "https://github.com/USER/REPO.git" 43 | } 44 | } 45 | ``` 46 | 47 | #### **Or** you may use special version if you create one 48 | 49 | Don't forget to replace **USER** and **REPO** to yours. 50 | 51 | ```json 52 | { 53 | "dependencies": { 54 | "your.package.name": "https://github.com/USER/REPO.git#v1.0.0" 55 | } 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /.github/workflows/test_pull_request.yml-sample: -------------------------------------------------------------------------------- 1 | name: test-pull-request 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [main, dev] 7 | types: [opened, synchronize, reopened] 8 | 9 | jobs: 10 | # --- EDIT MODE --- 11 | 12 | test-unity-2022-3-61f1-editmode: 13 | uses: ./.github/workflows/test_unity_plugin.yml 14 | with: 15 | projectPath: "./Unity-Package" 16 | unityVersion: "2022.3.61f1" 17 | testMode: "editmode" 18 | secrets: inherit 19 | 20 | test-unity-2023-2-20f1-editmode: 21 | uses: ./.github/workflows/test_unity_plugin.yml 22 | with: 23 | projectPath: "./Unity-Package" 24 | unityVersion: "2023.2.20f1" 25 | testMode: "editmode" 26 | secrets: inherit 27 | 28 | test-unity-6000-2-3f1-editmode: 29 | uses: ./.github/workflows/test_unity_plugin.yml 30 | with: 31 | projectPath: "./Unity-Package" 32 | unityVersion: "6000.2.3f1" 33 | testMode: "editmode" 34 | secrets: inherit 35 | 36 | # --- PLAY MODE --- 37 | 38 | test-unity-2022-3-61f1-playmode: 39 | uses: ./.github/workflows/test_unity_plugin.yml 40 | with: 41 | projectPath: "./Unity-Package" 42 | unityVersion: "2022.3.61f1" 43 | testMode: "playmode" 44 | secrets: inherit 45 | 46 | test-unity-2023-2-20f1-playmode: 47 | uses: ./.github/workflows/test_unity_plugin.yml 48 | with: 49 | projectPath: "./Unity-Package" 50 | unityVersion: "2023.2.20f1" 51 | testMode: "playmode" 52 | secrets: inherit 53 | 54 | test-unity-6000-2-3f1-playmode: 55 | uses: ./.github/workflows/test_unity_plugin.yml 56 | with: 57 | projectPath: "./Unity-Package" 58 | unityVersion: "6000.2.3f1" 59 | testMode: "playmode" 60 | secrets: inherit 61 | 62 | # --- STANDALONE --- 63 | 64 | test-unity-2022-3-61f1-standalone: 65 | uses: ./.github/workflows/test_unity_plugin.yml 66 | with: 67 | projectPath: "./Unity-Package" 68 | unityVersion: "2022.3.61f1" 69 | testMode: "standalone" 70 | secrets: inherit 71 | 72 | test-unity-2023-2-20f1-standalone: 73 | uses: ./.github/workflows/test_unity_plugin.yml 74 | with: 75 | projectPath: "./Unity-Package" 76 | unityVersion: "2023.2.20f1" 77 | testMode: "standalone" 78 | secrets: inherit 79 | 80 | test-unity-6000-2-3f1-standalone: 81 | uses: ./.github/workflows/test_unity_plugin.yml 82 | with: 83 | projectPath: "./Unity-Package" 84 | unityVersion: "6000.2.3f1" 85 | testMode: "standalone" 86 | secrets: inherit 87 | 88 | # ------------------- 89 | -------------------------------------------------------------------------------- /docs/Deploy-OpenUPM.md: -------------------------------------------------------------------------------- 1 | # Deploy your package to `OpenUPM` 2 | 3 | OpenUPM is a registry of package made for Unity. It takes package sources from GitHub repository. That is why your package repository should be public. And your package should be manually submitted for the first time to OpenUPM to be registered in the index. Time to time OpenUPM would fetch fresh data from GitHub to identify package updates if any. 4 | 5 | ### Do just once 6 | 7 | 1. [Submit package to OpenUPM registry](https://openupm.com/packages/add/). Use link to your GitHub repository. **This should be done just once!** 8 | 9 | ### Do each update 10 | 11 | ⚠️ Make sure you done editing `package.json` and files in `Assets/root` folder. Because it is going to be public with no ability to discard it. 12 | 13 | 1. Increment package version in the file `Assets/root/package.json`. It has `version` property. 14 | > Any further updates should be done with incrementing package version and making another GitHub release. 15 | > Versions lower than `1.0.0` is represented in Unity as "preview". 16 | 2. Create GitHub Release 17 | 1. Go to your GitHub repository 18 | 2. Click on `Releases` 19 | 3. Click on `Draft a new release` 20 | 4. Use `tag` that is equals to your package version name 21 | 22 | # Installation 23 | 24 | When your package is distributed, you can install it into any Unity project. 25 | 26 | - [Install OpenUPM-CLI](https://github.com/openupm/openupm-cli#installation) 27 | - Open the command line in the Unity project folder 28 | - Run the command 29 | 30 | ```bash 31 | openupm add YOUR_PACKAGE_NAME 32 | ``` 33 | 34 | ### Alternative manual installation 35 | 36 | If for any reason you don't want to use OpenUPM-CLI there is a manual approach. 37 | 38 | Modify `manifest.json` file. 39 | 40 | - Add the line into your's `dependencies` object 41 | - Change `your.package.name` to the name of your package 42 | - Replace **USER** and **REPO** to yours 43 | - Add `scopedRegistries` 44 | - Change `your.package.name` to the name of your package 45 | 46 | ```json 47 | { 48 | "dependencies": { 49 | "your.package.name": "1.0.0" 50 | }, 51 | "scopedRegistries": [ 52 | { 53 | "name": "OpenUPM", 54 | "url": "https://package.openupm.com", 55 | "scopes": [ 56 | "your.package.name" 57 | ] 58 | } 59 | ], 60 | } 61 | ``` 62 | 63 | --- 64 | 65 | ### Good to know 66 | 67 | > Make sure you made a new `Release` at GitHub 68 | > Make sure the GitHub repository is public 69 | > OpenUPM may take some time to index your new package or an update of your package. Usually up to 30 minutes. 70 | -------------------------------------------------------------------------------- /docs/Deploy-npmjs.md: -------------------------------------------------------------------------------- 1 | # Deploy your package to `npmjs.com` using `npm` 2 | 3 | `npmjs.com` is a very popular registry for wide range of packages. It was not designed to host Unity related packages, but it works. If for any reason still need to use it, there is the tutorial how to make it work. 4 | 5 | ### Preparation (just once) 6 | 7 | - Install [NPM](https://nodejs.org/en/download/) 8 | - Create [NPMJS](https://npmjs.com) account 9 | - Open `Commands` folder 10 | - Run the script `npm_add_user.bat` and sign-in to your account 11 | 12 |
13 | npm_add_user.bat script content 14 | 15 | It executes `npm adduser` command in the package root folder 16 | 17 | ```bash 18 | cd ..\Assets\root 19 | npm adduser 20 | ``` 21 | 22 |
23 | 24 | ### Deploy 25 | 26 | ⚠️ Make sure you done editing `package.json` and files in `Assets/root` folder. Because it is going to be public with no ability to discard it. 27 | 28 | 1. Increment `version` in `package.json` file 29 | > Any further updates should be done with incrementing package version. 30 | > Versions lower than `1.0.0` is represented in Unity as "preview". 31 | 2. Open `Commands` folder 32 | 3. Run the script `npm_deploy.bat` to publish your package to the public 33 | 34 |
35 | npm_deploy.bat script content 36 | 37 | The first line in the script copies the `README.md` file to the package root. Because the README should be in a package also, that is a part of the package format. 38 | It executes `npm publish` command in the package root folder. The command publishes your package to the NPMJS platform automatically 39 | 40 | ```bash 41 | xcopy ..\README.md ..\Assets\root\README.md* /Y 42 | xcopy ..\README.md ..\Assets\root\Documentation~\README.md* /Y 43 | cd ..\Assets\root 44 | npm publish 45 | pause 46 | ``` 47 | 48 |
49 | 50 | # Installation 51 | 52 | When your package is distributed, you can install it into any Unity project. 53 | 54 | - [Install OpenUPM-CLI](https://github.com/openupm/openupm-cli#installation) 55 | - Open the command line in the Unity project folder 56 | - Run the command 57 | 58 | ```bash 59 | openupm --registry https://registry.npmjs.org add YOUR_PACKAGE_NAME 60 | ``` 61 | 62 | ### Alternative manual installation 63 | 64 | If for any reason you don't want to use OpenUPM-CLI there is a manual approach. 65 | 66 | Modify `manifest.json` file. 67 | 68 | - Add the line into your's `dependencies` object 69 | - Change `your.package.name` to the name of your package 70 | - Replace **USER** and **REPO** to yours 71 | - Add `scopedRegistries` 72 | - Change `your.package.name` to the name of your package 73 | 74 | ```json 75 | { 76 | "dependencies": { 77 | "your.package.name": "1.0.0" 78 | }, 79 | "scopedRegistries": [ 80 | { 81 | "name": "npmjs", 82 | "url": "https://registry.npmjs.org", 83 | "scopes": [ 84 | "your.package.name" 85 | ] 86 | } 87 | ], 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/ManifestInstallerTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | ┌────────────────────────────────────────────────────────────────────────────┐ 3 | │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ 4 | │ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ 5 | │ Copyright (c) 2025 Ivan Murzak │ 6 | │ Licensed under the MIT License. │ 7 | │ See the LICENSE file in the project root for more information. │ 8 | └────────────────────────────────────────────────────────────────────────────┘ 9 | */ 10 | using System.IO; 11 | using NUnit.Framework; 12 | using UnityEngine; 13 | 14 | namespace YOUR_PACKAGE_ID.Installer.Tests 15 | { 16 | public class ManifestInstallerTests 17 | { 18 | const string PackageIdTag = "PACKAGE_ID"; 19 | const string PackageVersionTag = "PACKAGE_VERSION"; 20 | const string FilesRoot = "Assets/YOUR_PACKAGE_NAME_INSTALLER/Tests/Files"; 21 | const string FilesCopyRoot = "Temp/YOUR_PACKAGE_NAME_INSTALLER/Tests/Files"; 22 | static string CorrectManifestPath => $"{FilesRoot}/Correct/correct_manifest.json"; 23 | 24 | [SetUp] 25 | public void SetUp() 26 | { 27 | Debug.Log($"[{nameof(ManifestInstallerTests)}] SetUp"); 28 | Directory.CreateDirectory(FilesCopyRoot); 29 | } 30 | 31 | [TearDown] 32 | public void TearDown() 33 | { 34 | Debug.Log($"[{nameof(ManifestInstallerTests)}] TearDown"); 35 | 36 | // var files = Directory.GetFiles(FilesCopyRoot, "*.json", SearchOption.TopDirectoryOnly); 37 | // foreach (var file in files) 38 | // File.Delete(file); 39 | } 40 | 41 | [Test] 42 | public void All() 43 | { 44 | var files = Directory.GetFiles(FilesRoot, "*.json", SearchOption.TopDirectoryOnly); 45 | var correctManifest = File.ReadAllText(CorrectManifestPath) 46 | .Replace(PackageVersionTag, Installer.Version) 47 | .Replace(PackageIdTag, Installer.PackageId); 48 | 49 | foreach (var file in files) 50 | { 51 | Debug.Log($"Found JSON file: {file}"); 52 | 53 | // Copy the file 54 | var fileCopy = Path.Combine(FilesCopyRoot, Path.GetFileName(file)); 55 | File.Copy(file, fileCopy, overwrite: true); 56 | 57 | // Arrange 58 | File.WriteAllText(fileCopy, File.ReadAllText(fileCopy) 59 | .Replace(PackageVersionTag, Installer.Version) 60 | .Replace(PackageIdTag, Installer.PackageId)); 61 | 62 | // Act 63 | Installer.AddScopedRegistryIfNeeded(fileCopy); 64 | 65 | // Assert 66 | var modifiedManifest = File.ReadAllText(fileCopy); 67 | Assert.AreEqual(correctManifest, modifiedManifest, $"Modified manifest from {file} does not match the correct manifest."); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/test_unity_plugin.yml: -------------------------------------------------------------------------------- 1 | name: test-unity-plugin 2 | 3 | ############################################################################## 4 | # 1. Triggers 5 | ############################################################################## 6 | on: 7 | workflow_dispatch: 8 | workflow_call: 9 | inputs: 10 | projectPath: { required: true, type: string } 11 | unityVersion: { required: true, type: string } 12 | testMode: { required: true, type: string } 13 | secrets: 14 | UNITY_LICENSE: { required: true } 15 | UNITY_EMAIL: { required: true } 16 | UNITY_PASSWORD: { required: true } 17 | 18 | ############################################################################## 19 | # 2. Job – runs only after a maintainer applies the `ci-ok` label 20 | ############################################################################## 21 | jobs: 22 | test: 23 | if: | 24 | github.event_name != 'pull_request_target' || 25 | contains(github.event.pull_request.labels.*.name,'ci-ok') 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | os: [ubuntu-latest] 30 | platform: [base, windows-mono] 31 | 32 | name: ${{ inputs.unityVersion }} ${{ inputs.testMode }} on ${{ matrix.platform }} 33 | runs-on: ${{ matrix.os }} 34 | 35 | # permissions: # minimize the default token 36 | # contents: write 37 | # pull-requests: write 38 | 39 | steps: 40 | # --------------------------------------------------------------------- # 41 | # 2-a. (PR only) abort if the contributor also changed workflow files 42 | # --------------------------------------------------------------------- # 43 | - name: Abort if workflow files modified 44 | if: ${{ github.event_name == 'pull_request_target' }} 45 | run: | 46 | git fetch --depth=1 origin "${{ github.base_ref }}" 47 | if git diff --name-only HEAD origin/${{ github.base_ref }} | grep -q '^\.github/workflows/'; then 48 | echo "::error::This PR edits workflow files – refusing to run with secrets"; exit 1; 49 | fi 50 | 51 | # --------------------------------------------------------------------- # 52 | # 2-b. Checkout the contributor’s commit safely 53 | # --------------------------------------------------------------------- # 54 | - uses: actions/checkout@v6 55 | with: 56 | lfs: false 57 | 58 | # --------------------------------------------------------------------- # 59 | # 2-c. Cache & run the Unity test-runner 60 | # --------------------------------------------------------------------- # 61 | - uses: actions/cache@v4 62 | with: 63 | path: | 64 | ${{ inputs.projectPath }}/Library 65 | ~/.cache/unity3d 66 | key: Library-${{ inputs.unityVersion }}-${{ inputs.testMode }}-${{ matrix.platform }} 67 | 68 | # --------------------------------------------------------------------- # 69 | - name: Generate custom image name 70 | id: custom_image 71 | run: echo "image=unityci/editor:ubuntu-${{ inputs.unityVersion }}-${{ matrix.platform }}-3" >> $GITHUB_OUTPUT 72 | shell: bash 73 | 74 | - uses: game-ci/unity-test-runner@v4 75 | id: tests 76 | env: 77 | UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} 78 | UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} 79 | UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} 80 | with: 81 | projectPath: ${{ inputs.projectPath }} 82 | unityVersion: ${{ inputs.unityVersion }} 83 | testMode: ${{ inputs.testMode }} 84 | customImage: ${{ steps.custom_image.outputs.image }} 85 | githubToken: ${{ secrets.GITHUB_TOKEN }} 86 | checkName: ${{ inputs.unityVersion }} ${{ inputs.testMode }} ${{ matrix.platform }} Test Results 87 | artifactsPath: artifacts-${{ inputs.unityVersion }}-${{ inputs.testMode }}-${{ matrix.platform }} 88 | customParameters: -CI true -GITHUB_ACTIONS true 89 | 90 | # --------------------------------------------------------------------- # 91 | - uses: actions/upload-artifact@v4 92 | if: always() 93 | with: 94 | name: Test results for ${{ inputs.unityVersion }} ${{ inputs.testMode }} on ${{ matrix.platform }} 95 | path: ${{ steps.tests.outputs.artifactsPath }} 96 | -------------------------------------------------------------------------------- /commands/init.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | <# 3 | .SYNOPSIS 4 | Initializes the Unity Package project by replacing placeholders. 5 | 6 | .DESCRIPTION 7 | Replaces placeholders in file content, filenames, and directory names. 8 | Placeholders: 9 | - YOUR_PACKAGE_ID 10 | - YOUR_PACKAGE_ID_LOWERCASE 11 | - YOUR_PACKAGE_NAME 12 | - YOUR_PACKAGE_NAME_INSTALLER 13 | - YOUR_PACKAGE_NAME_INSTALLER_FILE 14 | 15 | .PARAMETER PackageId 16 | The package ID (e.g., "com.company.package"). 17 | 18 | .PARAMETER PackageName 19 | The package name (e.g., "My Package"). 20 | 21 | .EXAMPLE 22 | .\init.ps1 -PackageId "com.mycompany.coolpackage" -PackageName "Cool Package" 23 | #> 24 | 25 | param( 26 | [Parameter(Mandatory = $true)] 27 | [string]$PackageId, 28 | 29 | [Parameter(Mandatory = $true)] 30 | [string]$PackageName 31 | ) 32 | 33 | $ErrorActionPreference = "Stop" 34 | 35 | # Derived variables 36 | $PackageIdLowercase = $PackageId.ToLower() 37 | $PackageNameInstaller = "$PackageName Installer" 38 | $PackageNameInstallerFile = $PackageNameInstaller -replace ' ', '-' 39 | 40 | # Replacements map 41 | $Replacements = @{ 42 | "YOUR_PACKAGE_ID" = $PackageId 43 | "YOUR_PACKAGE_ID_LOWERCASE" = $PackageIdLowercase 44 | "YOUR_PACKAGE_NAME" = $PackageName 45 | "YOUR_PACKAGE_NAME_INSTALLER_FILE" = $PackageNameInstallerFile 46 | "YOUR_PACKAGE_NAME_INSTALLER" = $PackageNameInstaller 47 | } 48 | 49 | # Sort keys by length descending to avoid partial replacements 50 | $SortedKeys = $Replacements.Keys | Sort-Object { $_.Length } -Descending 51 | 52 | # Root directory (parent of commands) 53 | $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path 54 | $RepoRoot = Split-Path -Parent $ScriptDir 55 | 56 | Write-Host "Initializing package with:" -ForegroundColor Cyan 57 | Write-Host " ID: $PackageId" 58 | Write-Host " Name: $PackageName" 59 | Write-Host " Installer: $PackageNameInstaller" 60 | Write-Host " Installer File: $PackageNameInstallerFile" 61 | Write-Host "" 62 | 63 | # Define target paths 64 | $TargetPaths = @("Installer", "Unity-Package", "README.md") 65 | 66 | # 1. Replace content in files 67 | Write-Host "Replacing content in files..." -ForegroundColor Yellow 68 | $Files = @() 69 | foreach ($Path in $TargetPaths) { 70 | $FullPath = Join-Path $RepoRoot $Path 71 | if (Test-Path $FullPath) { 72 | if ((Get-Item $FullPath).PSIsContainer) { 73 | $Files += Get-ChildItem -Path $FullPath -Recurse -File 74 | } 75 | else { 76 | $Files += Get-Item $FullPath 77 | } 78 | } 79 | else { 80 | Write-Warning "Path not found: $FullPath" 81 | } 82 | } 83 | 84 | foreach ($File in $Files) { 85 | $Content = Get-Content -Path $File.FullName -Raw 86 | $NewContent = $Content 87 | $Modified = $false 88 | 89 | foreach ($Key in $SortedKeys) { 90 | if ($NewContent -match $Key) { 91 | $NewContent = $NewContent -replace $Key, $Replacements[$Key] 92 | $Modified = $true 93 | } 94 | } 95 | 96 | if ($Modified) { 97 | Set-Content -Path $File.FullName -Value $NewContent -NoNewline 98 | Write-Host " Updated: $($File.FullName)" -ForegroundColor Gray 99 | } 100 | } 101 | 102 | # 2. Rename files and directories 103 | # We need to do this depth-first (bottom-up) so we don't rename a parent directory before its children 104 | Write-Host "Renaming files and directories..." -ForegroundColor Yellow 105 | $Items = @() 106 | foreach ($Path in $TargetPaths) { 107 | $FullPath = Join-Path $RepoRoot $Path 108 | if (Test-Path $FullPath) { 109 | if ((Get-Item $FullPath).PSIsContainer) { 110 | $Items += Get-ChildItem -Path $FullPath -Recurse 111 | } 112 | else { 113 | $Items += Get-Item $FullPath 114 | } 115 | } 116 | } 117 | 118 | # Sort depth-first (longest path first) to handle nested renames correctly 119 | $Items = $Items | Sort-Object -Property FullName -Descending | Select-Object -Unique 120 | 121 | foreach ($Item in $Items) { 122 | $NewName = $Item.Name 123 | foreach ($Key in $SortedKeys) { 124 | if ($NewName -match $Key) { 125 | $NewName = $NewName -replace $Key, $Replacements[$Key] 126 | } 127 | } 128 | 129 | if ($NewName -ne $Item.Name) { 130 | Rename-Item -Path $Item.FullName -NewName $NewName 131 | Write-Host " Renamed: $($Item.Name) -> $NewName" -ForegroundColor Gray 132 | } 133 | } 134 | 135 | Write-Host "Done!" -ForegroundColor Green 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Unity Package Template](https://github.com/IvanMurzak/Unity-Package-Template) 2 | 3 | Stats 4 | 5 | Unity Editor supports NPM packages. It is way more flexible solution in comparison with classic Plugin that Unity is using for years. NPM package supports versioning and dependencies. You may update / downgrade any package very easily. Also, Unity Editor has UPM (Unity Package Manager) that makes the process even simpler. 6 | 7 | This template repository is designed to be easily updated into a real Unity package. Please follow the instruction bellow, it will help you to go through the entire process of package creation, distribution and installing. 8 | 9 | # Steps to make your package 10 | 11 | #### 1️⃣ Click the button to create new repository on GitHub using this template. 12 | 13 | [![create new repository](https://user-images.githubusercontent.com/9135028/198753285-3d3c9601-0711-43c7-a8f2-d40ec42393a2.png)](https://github.com/IvanMurzak/Unity-Package-Template/generate) 14 | 15 | #### 2️⃣ Clone your new repository and open it in Unity Editor 16 | 17 | #### 3️⃣ Initialize Project 18 | 19 | Use the initialization script to rename the package and replace all placeholders. 20 | 21 | ```powershell 22 | .\commands\init.ps1 -PackageId "com.company.package" -PackageName "My Package" 23 | ``` 24 | 25 | This script will: 26 | - Rename directories and files. 27 | - Replace `YOUR_PACKAGE_ID`, `YOUR_PACKAGE_NAME`, etc. in all files. 28 | 29 | #### 4️⃣ Manual Configuration 30 | 31 | 1. **Update `package.json`** 32 | Open `Unity-Package/Assets/root/package.json` and update: 33 | - `description` 34 | - `author` 35 | - `keywords` 36 | - `unity` (minimum supported Unity version) 37 | 38 | 2. **Generate Meta Files** 39 | To ensure all Unity meta files are correctly generated: 40 | - Open Unity Hub. 41 | - Add the `Installer` folder as a project. 42 | - Add the `Unity-Package` folder as a project. 43 | - Open both projects in Unity Editor. This will generate the necessary `.meta` files. 44 | 45 | #### 5️⃣ Version Management 46 | 47 | To update the package version across all files (package.json, Installer.cs, etc.), use the bump version script: 48 | 49 | ```powershell 50 | .\commands\bump-version.ps1 -NewVersion "1.0.1" 51 | ``` 52 | 53 | #### 6️⃣ Setup CI/CD 54 | 55 | To enable automatic testing and deployment: 56 | 57 | 1. **Configure GitHub Secrets** 58 | Go to `Settings` > `Secrets and variables` > `Actions` > `New repository secret` and add: 59 | - `UNITY_EMAIL`: Your Unity account email. 60 | - `UNITY_PASSWORD`: Your Unity account password. 61 | - `UNITY_LICENSE`: Content of your `Unity_lic.ulf` file. 62 | - Windows: `C:/ProgramData/Unity/Unity_lic.ulf` 63 | - Mac: `/Library/Application Support/Unity/Unity_lic.ulf` 64 | - Linux: `~/.local/share/unity3d/Unity/Unity_lic.ulf` 65 | 66 | 2. **Enable Workflows** 67 | Rename the sample workflow files to enable them: 68 | - `.github/workflows/release.yml-sample` ➡️ `.github/workflows/release.yml` 69 | - `.github/workflows/test_pull_request.yml-sample` ➡️ `.github/workflows/test_pull_request.yml` 70 | 71 | 3. **Update Unity Version** 72 | Open both `.yml` files and update the `UNITY_VERSION` (or similar variable) to match your project's Unity Editor version. 73 | 74 | 4. **Automatic Deployment** 75 | The release workflow triggers automatically when you push to the `main` branch with an incremented version in `package.json`. 76 | 77 | #### 7️⃣ Add files into `Assets/root` folder 78 | 79 | [Unity guidelines](https://docs.unity3d.com/Manual/cus-layout.html) about organizing files into the package root directory 80 | 81 | ```text 82 | 83 | ├── package.json 84 | ├── README.md 85 | ├── CHANGELOG.md 86 | ├── LICENSE.md 87 | ├── Third Party Notices.md 88 | ├── Editor 89 | │ ├── [company-name].[package-name].Editor.asmdef 90 | │ └── EditorExample.cs 91 | ├── Runtime 92 | │ ├── [company-name].[package-name].asmdef 93 | │ └── RuntimeExample.cs 94 | ├── Tests 95 | │ ├── Editor 96 | │ │ ├── [company-name].[package-name].Editor.Tests.asmdef 97 | │ │ └── EditorExampleTest.cs 98 | │ └── Runtime 99 | │ ├── [company-name].[package-name].Tests.asmdef 100 | │ └── RuntimeExampleTest.cs 101 | ├── Samples~ 102 | │ ├── SampleFolder1 103 | │ ├── SampleFolder2 104 | │ └── ... 105 | └── Documentation~ 106 | └── [package-name].md 107 | ``` 108 | 109 | ##### Final polishing 110 | 111 | - Update the `README.md` file (this file) with information about your package. 112 | - Copy the updated `README.md` to `Assets/root` as well. 113 | 114 | > ⚠️ Everything outside of the `root` folder won't be added to your package. But still could be used for testing or showcasing your package at your repository. 115 | 116 | #### 8️⃣ Deploy to any registry you like 117 | 118 | - [Deploy to OpenUPM](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-OpenUPM.md) (recommended) 119 | - [Deploy using GitHub](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-GitHub.md) 120 | - [Deploy to npmjs.com](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-npmjs.md) 121 | 122 | #### 9️⃣ Install your package into Unity Project 123 | 124 | When your package is distributed, you can install it into any Unity project. 125 | 126 | > Don't install into the same Unity project, please use another one. 127 | 128 | - [Install OpenUPM-CLI](https://github.com/openupm/openupm-cli#installation) 129 | - Open a command line at the root of Unity project (the folder which contains `Assets`) 130 | - Execute the command (for `OpenUPM` hosted package) 131 | 132 | ```bash 133 | openupm add YOUR_PACKAGE_NAME 134 | ``` 135 | 136 | # Final view in Unity Package Manager 137 | 138 | ![image](https://user-images.githubusercontent.com/9135028/198777922-fdb71949-aee7-49c8-800f-7db885de9453.png) 139 | -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/README.md: -------------------------------------------------------------------------------- 1 | # Unity Package Template 2 | 3 | Stats 4 | 5 | Unity Editor supports NPM packages. It is way more flexible solution in comparison with classic Plugin that Unity is using for years. NPM package supports versioning and dependencies. You may update / downgrade any package very easily. Also, Unity Editor has UPM (Unity Package Manager) that makes the process even simpler. 6 | 7 | This template repository is designed to be easily updated into a real Unity package. Please follow the instruction bellow, it will help you to go through the entire process of package creation, distribution and installing. 8 | 9 | # Steps to make your package 10 | 11 | #### 1️⃣ Click the button to create new repository on GitHub using this template. 12 | 13 | [![create new repository](https://user-images.githubusercontent.com/9135028/198753285-3d3c9601-0711-43c7-a8f2-d40ec42393a2.png)](https://github.com/IvanMurzak/Unity-Package-Template/generate) 14 | 15 | #### 2️⃣ Clone your new repository and open it in Unity Editor 16 | 17 | #### 3️⃣ Rename `Package` 18 | 19 | Your package should have unique identifier. It is called a `name` of the package. It support only limited symbols. There is a sample of the package name. 20 | 21 | ```text 22 | com.github.your_name.package 23 | ``` 24 | 25 | - 👉 Instead of the word `package` use a word or couple of words that explains the main purpose of the package. 26 | - 👉 The `name` should be unique in the world. 27 | 28 | ###### Option 1: Use script to rename package (recommended) 29 | 30 | For MacOS 31 | 32 | ```bash 33 | 34 | ``` 35 | 36 | For Windows 37 | 38 | ```bash 39 | cd Commands 40 | .\package_rename.bat Username PackageName 41 | ``` 42 | 43 | ###### Option 2: Manual package rename 44 | 45 | Follow the instruction - [manual package rename](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Manual-Package-Rename.md) 46 | 47 | 48 | #### 3️⃣ Customize `Assets/root/package.json` 49 | 50 | - 👉 **Update** `name` 51 | > Sample: `com.github.your_name.package` 52 | > Instead of the word `package` use a word or couple of words that explains the main purpose of the package. 53 | > The `name` should be unique in the world. 54 | 55 | - 👉 **Update** `unity` to setup minimum supported Unity version 56 | - 👉 **Update** 57 | - `displayName` - visible name of the package, 58 | - `version` - the version of the package (1.0.0), 59 | - `description` - short description of the package, 60 | - `author` - author of the package and url to the author (could be GitHub profile), 61 | - `keywords` - array of keywords that describes the package. 62 | 63 | #### 4️⃣ Do you need Tests? 64 | 65 |
66 | ❌ NO 67 | 68 | - 👉 **Delete** `Assets/root/Tests` folder 69 | - 👉 **Delete** `.github/workflows` folder 70 | 71 |
72 | 73 |
74 | ✅ YES 75 | 76 | - 👉 Make sure you executed `package-rename` script from the step #2. If not, please follow [manual package rename](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Manual-Package-Rename.md) instructions 77 | 78 | - 👉 Add GitHub Secrets 79 | > At the GitHub repository, go to "Settings", then "Secrets and Variables", then "Actions", then click on "New repository secret" 80 | 1. Add `UNITY_EMAIL` - email of your Unity ID's account 81 | 2. Add `UNITY_PASSWORD` - password of your Unity ID's account 82 | 3. Add `UNITY_LICENSE` - license content. Could be taken from `Unity_lic.ulf` file. Just open it in any text editor and copy the entire content 83 | 1. Windows: The `Unity_lic.ulf` file is located at `C:/ProgramData/Unity/Unity_lic.ulf` 84 | 2. MacOS: `/Library/Application Support/Unity/Unity_lic.ulf` 85 | 3. Linux: `~/.local/share/unity3d/Unity/Unity_lic.ulf` 86 | 87 |
88 | 89 | #### 4️⃣ Add files into `Assets/root` folder 90 | 91 | [Unity guidelines](https://docs.unity3d.com/Manual/cus-layout.html) about organizing files into the package root directory 92 | 93 | ```text 94 | 95 | ├── package.json 96 | ├── README.md 97 | ├── CHANGELOG.md 98 | ├── LICENSE.md 99 | ├── Third Party Notices.md 100 | ├── Editor 101 | │ ├── [company-name].[package-name].Editor.asmdef 102 | │ └── EditorExample.cs 103 | ├── Runtime 104 | │ ├── [company-name].[package-name].asmdef 105 | │ └── RuntimeExample.cs 106 | ├── Tests 107 | │ ├── Editor 108 | │ │ ├── [company-name].[package-name].Editor.Tests.asmdef 109 | │ │ └── EditorExampleTest.cs 110 | │ └── Runtime 111 | │ ├── [company-name].[package-name].Tests.asmdef 112 | │ └── RuntimeExampleTest.cs 113 | ├── Samples~ 114 | │ ├── SampleFolder1 115 | │ ├── SampleFolder2 116 | │ └── ... 117 | └── Documentation~ 118 | └── [package-name].md 119 | ``` 120 | 121 | ##### Final polishing 122 | 123 | - Update the `README.md` file (this file) with information about your package. 124 | - Copy the updated `README.md` to `Assets/root` as well. 125 | 126 | > ⚠️ Everything outside of the `root` folder won't be added to your package. But still could be used for testing or showcasing your package at your repository. 127 | 128 | #### 5️⃣ Deploy to any registry you like 129 | 130 | - [Deploy to OpenUPM](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-OpenUPM.md) (recommended) 131 | - [Deploy using GitHub](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-GitHub.md) 132 | - [Deploy to npmjs.com](https://github.com/IvanMurzak/Unity-Package-Template/blob/main/Docs/Deploy-npmjs.md) 133 | 134 | #### 6️⃣ Install your package into Unity Project 135 | 136 | When your package is distributed, you can install it into any Unity project. 137 | 138 | > Don't install into the same Unity project, please use another one. 139 | 140 | - [Install OpenUPM-CLI](https://github.com/openupm/openupm-cli#installation) 141 | - Open a command line at the root of Unity project (the folder which contains `Assets`) 142 | - Execute the command (for `OpenUPM` hosted package) 143 | 144 | ```bash 145 | openupm add YOUR_PACKAGE_NAME 146 | ``` 147 | 148 | # Final view in Unity Package Manager 149 | 150 | ![image](https://user-images.githubusercontent.com/9135028/198777922-fdb71949-aee7-49c8-800f-7db885de9453.png) 151 | -------------------------------------------------------------------------------- /commands/bump-version.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | <# 3 | .SYNOPSIS 4 | Automated version bumping script for Unity Package project 5 | 6 | .DESCRIPTION 7 | Updates version numbers across all project files automatically to prevent human errors. 8 | Supports preview mode for safe testing. 9 | 10 | .PARAMETER NewVersion 11 | The new version number in semver format (e.g., "0.18.0") 12 | 13 | .PARAMETER WhatIf 14 | Preview changes without applying them 15 | 16 | .EXAMPLE 17 | .\bump-version.ps1 -NewVersion "0.18.0" 18 | 19 | .EXAMPLE 20 | .\bump-version.ps1 -NewVersion "0.18.0" -WhatIf 21 | #> 22 | 23 | param( 24 | [Parameter(Mandatory = $true)] 25 | [string]$NewVersion, 26 | 27 | [switch]$WhatIf 28 | ) 29 | 30 | # Set location to repository root (parent of commands folder) 31 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path 32 | $repoRoot = Split-Path -Parent $scriptDir 33 | Push-Location $repoRoot 34 | 35 | # Script configuration 36 | $ErrorActionPreference = "Stop" 37 | 38 | # Version file locations (relative to script root) 39 | $VersionFiles = @( 40 | @{ 41 | Path = "Unity-Package/Assets/root/package.json" 42 | Pattern = '"version":\s*"[\d\.]+"' 43 | Replace = '"version": "{VERSION}"' 44 | Description = "Unity package version" 45 | }, 46 | @{ 47 | Path = "Installer/Assets/YOUR_PACKAGE_NAME_INSTALLER/Installer.cs" 48 | Pattern = 'public const string Version = "[\d\.]+";' 49 | Replace = 'public const string Version = "{VERSION}";' 50 | Description = "Installer C# version constant" 51 | } 52 | ) 53 | 54 | function Write-ColorText { 55 | param([string]$Text, [string]$Color = "White") 56 | Write-Host $Text -ForegroundColor $Color 57 | } 58 | 59 | function Test-SemanticVersion { 60 | param([string]$Version) 61 | 62 | if ([string]::IsNullOrWhiteSpace($Version)) { 63 | return $false 64 | } 65 | 66 | # Basic semver pattern: major.minor.patch (with optional prerelease/build) 67 | $pattern = '^\d+\.\d+\.\d+(-[a-zA-Z0-9\-\.]+)?(\+[a-zA-Z0-9\-\.]+)?$' 68 | return $Version -match $pattern 69 | } 70 | 71 | function Get-CurrentVersion { 72 | # Extract current version from package.json 73 | $packageJsonPath = "Unity-Package/Assets/root/package.json" 74 | if (-not (Test-Path $packageJsonPath)) { 75 | throw "Could not find package.json at: $packageJsonPath" 76 | } 77 | 78 | $content = Get-Content $packageJsonPath -Raw 79 | if ($content -match '"version":\s*"([\d\.]+)"') { 80 | return $Matches[1] 81 | } 82 | 83 | throw "Could not extract current version from package.json" 84 | } 85 | 86 | function Update-VersionFiles { 87 | param([string]$OldVersion, [string]$NewVersion, [bool]$PreviewOnly = $false) 88 | 89 | $changes = @() 90 | 91 | foreach ($file in $VersionFiles) { 92 | $fullPath = $file.Path 93 | 94 | if (-not (Test-Path $fullPath)) { 95 | Write-ColorText "⚠️ File not found: $($file.Path)" "Yellow" 96 | continue 97 | } 98 | 99 | $content = Get-Content $fullPath -Raw 100 | $originalContent = $content 101 | 102 | # Create the replacement string 103 | $replacement = $file.Replace -replace '\{VERSION\}', $NewVersion 104 | 105 | # Apply the replacement 106 | $newContent = $content -replace $file.Pattern, $replacement 107 | 108 | # Check if any changes were made 109 | if ($originalContent -ne $newContent) { 110 | # Count matches for reporting 111 | $regexMatches = [regex]::Matches($originalContent, $file.Pattern) 112 | 113 | $changes += @{ 114 | Path = $file.Path 115 | Description = $file.Description 116 | Matches = $regexMatches.Count 117 | Content = $newContent 118 | OriginalContent = $originalContent 119 | } 120 | 121 | Write-ColorText "📝 $($file.Description): $($regexMatches.Count) occurrence(s)" "Green" 122 | 123 | # Show the actual changes 124 | foreach ($match in $regexMatches) { 125 | $newValue = $match.Value -replace $file.Pattern, $replacement 126 | Write-ColorText " $($match.Value) → $newValue" "Gray" 127 | } 128 | } 129 | else { 130 | Write-ColorText "⚠️ No matches found in: $($file.Path)" "Yellow" 131 | Write-ColorText " Pattern: $($file.Pattern)" "Gray" 132 | } 133 | } 134 | 135 | if ($changes.Count -eq 0) { 136 | Write-ColorText "❌ No version references found to update!" "Red" 137 | Pop-Location 138 | exit 1 139 | } 140 | 141 | if ($PreviewOnly) { 142 | Write-ColorText "`n📋 Preview Summary:" "Cyan" 143 | Write-ColorText "Files to be modified: $($changes.Count)" "White" 144 | Write-ColorText "Total replacements: $(($changes | Measure-Object -Property Matches -Sum).Sum)" "White" 145 | return $null 146 | } 147 | 148 | # Apply changes 149 | foreach ($change in $changes) { 150 | $fullPath = $change.Path 151 | Set-Content -Path $fullPath -Value $change.Content -NoNewline 152 | } 153 | 154 | return $changes 155 | } 156 | 157 | # Main execution 158 | try { 159 | Write-ColorText "🚀 Package Version Bump Script" "Cyan" 160 | Write-ColorText "=================================" "Cyan" 161 | 162 | # Validate semantic version format 163 | if (-not (Test-SemanticVersion $NewVersion)) { 164 | Write-ColorText "❌ Invalid semantic version format: $NewVersion" "Red" 165 | Write-ColorText "Expected format: major.minor.patch (e.g., '1.2.3')" "Yellow" 166 | Pop-Location 167 | exit 1 168 | } 169 | 170 | # Get current version 171 | $currentVersion = Get-CurrentVersion 172 | Write-ColorText "📋 Current version: $currentVersion" "White" 173 | Write-ColorText "📋 New version: $NewVersion" "White" 174 | 175 | if ($currentVersion -eq $NewVersion) { 176 | Write-ColorText "⚠️ New version is the same as current version" "Yellow" 177 | Pop-Location 178 | exit 0 179 | } 180 | 181 | Write-ColorText "`n🔍 Scanning for version references..." "Cyan" 182 | 183 | # Update version files 184 | $changes = Update-VersionFiles -OldVersion $currentVersion -NewVersion $NewVersion -PreviewOnly $WhatIf 185 | 186 | if ($WhatIf) { 187 | Write-ColorText "`n✅ Preview completed. Use without -WhatIf to apply changes." "Green" 188 | Pop-Location 189 | exit 0 190 | } 191 | 192 | if ($changes -and $changes.Count -gt 0) { 193 | Write-ColorText "`n🎉 Version bump completed successfully!" "Green" 194 | Write-ColorText " Updated $($changes.Count) files" "White" 195 | Write-ColorText " Total replacements: $(($changes | Measure-Object -Property Matches -Sum).Sum)" "White" 196 | Write-ColorText " Version: $currentVersion → $NewVersion" "White" 197 | Write-ColorText "`n💡 Remember to commit these changes to git" "Cyan" 198 | } 199 | 200 | Pop-Location 201 | } 202 | catch { 203 | Write-ColorText "`n❌ Script failed: $($_.Exception.Message)" "Red" 204 | Pop-Location 205 | exit 1 206 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Installer.Manifest.cs: -------------------------------------------------------------------------------- 1 | /* 2 | ┌────────────────────────────────────────────────────────────────────────────┐ 3 | │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ 4 | │ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ 5 | │ Copyright (c) 2025 Ivan Murzak │ 6 | │ Licensed under the MIT License. │ 7 | │ See the LICENSE file in the project root for more information. │ 8 | └────────────────────────────────────────────────────────────────────────────┘ 9 | */ 10 | #nullable enable 11 | using System.IO; 12 | using System.Linq; 13 | using UnityEngine; 14 | using YOUR_PACKAGE_ID.Installer.SimpleJSON; 15 | using System.Runtime.CompilerServices; 16 | 17 | [assembly: InternalsVisibleTo("YOUR_PACKAGE_ID.Installer.Tests")] 18 | namespace YOUR_PACKAGE_ID.Installer 19 | { 20 | public static partial class Installer 21 | { 22 | static string ManifestPath => Path.Combine(Application.dataPath, "../Packages/manifest.json"); 23 | 24 | // Property names 25 | public const string Dependencies = "dependencies"; 26 | public const string ScopedRegistries = "scopedRegistries"; 27 | public const string Name = "name"; 28 | public const string Url = "url"; 29 | public const string Scopes = "scopes"; 30 | 31 | // Property values 32 | public const string RegistryName = "package.openupm.com"; 33 | public const string RegistryUrl = "https://package.openupm.com"; 34 | public static readonly string[] PackageIds = new string[] { 35 | "com.ivanmurzak", // Ivan Murzak's OpenUPM packages 36 | "extensions.unity", // Ivan Murzak's OpenUPM packages (older) 37 | "org.nuget.com.ivanmurzak", // Ivan Murzak's NuGet packages 38 | "org.nuget.microsoft", // Microsoft NuGet packages 39 | "org.nuget.system", // Microsoft NuGet packages 40 | "org.nuget.r3" // R3 package NuGet package 41 | }; 42 | 43 | /// 44 | /// Determines if the version should be updated. Only update if installer version is higher than current version. 45 | /// 46 | /// Current package version string 47 | /// Installer version string 48 | /// True if version should be updated (installer version is higher), false otherwise 49 | 50 | internal static bool ShouldUpdateVersion(string currentVersion, string installerVersion) 51 | { 52 | if (string.IsNullOrEmpty(currentVersion)) 53 | return true; // No current version, should install 54 | 55 | if (string.IsNullOrEmpty(installerVersion)) 56 | return false; // No installer version, don't change 57 | 58 | try 59 | { 60 | // Try to parse as System.Version (semantic versioning) 61 | var current = new System.Version(currentVersion); 62 | var installer = new System.Version(installerVersion); 63 | 64 | // Only update if installer version is higher than current version 65 | return installer > current; 66 | } 67 | catch (System.Exception) 68 | { 69 | Debug.LogWarning($"Failed to parse versions '{currentVersion}' or '{installerVersion}' as System.Version."); 70 | // If version parsing fails, fall back to string comparison 71 | // This ensures we don't break if version format is unexpected 72 | return string.Compare(installerVersion, currentVersion, System.StringComparison.OrdinalIgnoreCase) > 0; 73 | } 74 | } 75 | 76 | public static void AddScopedRegistryIfNeeded(string manifestPath, int indent = 2) 77 | { 78 | if (!File.Exists(manifestPath)) 79 | { 80 | Debug.LogError($"{manifestPath} not found!"); 81 | return; 82 | } 83 | var jsonText = File.ReadAllText(manifestPath) 84 | .Replace("{ }", "{\n}") 85 | .Replace("{}", "{\n}") 86 | .Replace("[ ]", "[\n]") 87 | .Replace("[]", "[\n]"); 88 | 89 | var manifestJson = JSONObject.Parse(jsonText); 90 | if (manifestJson == null) 91 | { 92 | Debug.LogError($"Failed to parse {manifestPath} as JSON."); 93 | return; 94 | } 95 | 96 | var modified = false; 97 | 98 | // --- Add scoped registries if needed 99 | var scopedRegistries = manifestJson[ScopedRegistries]; 100 | if (scopedRegistries == null) 101 | { 102 | manifestJson[ScopedRegistries] = new JSONArray(); 103 | modified = true; 104 | } 105 | 106 | // --- Add OpenUPM registry if needed 107 | var openUpmRegistry = scopedRegistries!.Linq 108 | .Select(kvp => kvp.Value) 109 | .Where(r => r.Linq 110 | .Any(p => p.Key == Name && p.Value == RegistryName)) 111 | .FirstOrDefault(); 112 | 113 | if (openUpmRegistry == null) 114 | { 115 | scopedRegistries.Add(openUpmRegistry = new JSONObject 116 | { 117 | [Name] = RegistryName, 118 | [Url] = RegistryUrl, 119 | [Scopes] = new JSONArray() 120 | }); 121 | modified = true; 122 | } 123 | 124 | // --- Add missing scopes 125 | var scopes = openUpmRegistry[Scopes]; 126 | if (scopes == null) 127 | { 128 | openUpmRegistry[Scopes] = scopes = new JSONArray(); 129 | modified = true; 130 | } 131 | foreach (var packageId in PackageIds) 132 | { 133 | var existingScope = scopes!.Linq 134 | .Select(kvp => kvp.Value) 135 | .Where(value => value == packageId) 136 | .FirstOrDefault(); 137 | if (existingScope == null) 138 | { 139 | scopes.Add(packageId); 140 | modified = true; 141 | } 142 | } 143 | 144 | // --- Package Dependency (Version-aware installation) 145 | // Only update version if installer version is higher than current version 146 | // This prevents downgrades when users manually update to newer versions 147 | var dependencies = manifestJson[Dependencies]; 148 | if (dependencies == null) 149 | { 150 | manifestJson[Dependencies] = dependencies = new JSONObject(); 151 | modified = true; 152 | } 153 | 154 | // Only update version if installer version is higher than current version 155 | var currentVersion = dependencies[PackageId]; 156 | if (currentVersion == null || ShouldUpdateVersion(currentVersion, Version)) 157 | { 158 | dependencies[PackageId] = Version; 159 | modified = true; 160 | } 161 | 162 | // --- Write changes back to manifest 163 | if (modified) 164 | File.WriteAllText(manifestPath, manifestJson.ToString(indent).Replace("\" : ", "\": ")); 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/Tests/VersionComparisonTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | ┌────────────────────────────────────────────────────────────────────────────┐ 3 | │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ 4 | │ Repository: GitHub (https://github.com/IvanMurzak/Unity-Package-Template) │ 5 | │ Copyright (c) 2025 Ivan Murzak │ 6 | │ Licensed under the MIT License. │ 7 | │ See the LICENSE file in the project root for more information. │ 8 | └────────────────────────────────────────────────────────────────────────────┘ 9 | */ 10 | using System.IO; 11 | using NUnit.Framework; 12 | using YOUR_PACKAGE_ID.Installer.SimpleJSON; 13 | 14 | namespace YOUR_PACKAGE_ID.Installer.Tests 15 | { 16 | public class VersionComparisonTests 17 | { 18 | const string TestManifestPath = "Temp/YOUR_PACKAGE_ID.Installer.Tests/test_manifest.json"; 19 | const string PackageId = "YOUR_PACKAGE_ID_LOWERCASE"; 20 | 21 | [SetUp] 22 | public void SetUp() 23 | { 24 | var dir = Path.GetDirectoryName(TestManifestPath); 25 | if (!Directory.Exists(dir)) 26 | Directory.CreateDirectory(dir); 27 | } 28 | 29 | [TearDown] 30 | public void TearDown() 31 | { 32 | if (File.Exists(TestManifestPath)) 33 | File.Delete(TestManifestPath); 34 | } 35 | 36 | [Test] 37 | public void ShouldUpdateVersion_PatchVersionHigher_ReturnsTrue() 38 | { 39 | // Act & Assert 40 | Assert.IsTrue( 41 | condition: Installer.ShouldUpdateVersion( 42 | currentVersion: "1.5.1", 43 | installerVersion: "1.5.2" 44 | ), 45 | message: "Should update when patch version is higher" 46 | ); 47 | } 48 | 49 | [Test] 50 | public void ShouldUpdateVersion_PatchVersionLower_ReturnsFalse() 51 | { 52 | // Act & Assert 53 | Assert.IsFalse( 54 | condition: Installer.ShouldUpdateVersion( 55 | currentVersion: "1.5.2", 56 | installerVersion: "1.5.1" 57 | ), 58 | message: "Should not downgrade when patch version is lower" 59 | ); 60 | } 61 | 62 | [Test] 63 | public void ShouldUpdateVersion_MinorVersionHigher_ReturnsTrue() 64 | { 65 | // Act & Assert 66 | Assert.IsTrue( 67 | condition: Installer.ShouldUpdateVersion( 68 | currentVersion: "1.5.0", 69 | installerVersion: "1.6.0" 70 | ), 71 | message: "Should update when minor version is higher" 72 | ); 73 | } 74 | 75 | [Test] 76 | public void ShouldUpdateVersion_MinorVersionLower_ReturnsFalse() 77 | { 78 | // Act & Assert 79 | Assert.IsFalse( 80 | condition: Installer.ShouldUpdateVersion( 81 | currentVersion: "1.6.0", 82 | installerVersion: "1.5.0" 83 | ), 84 | message: "Should not downgrade when minor version is lower" 85 | ); 86 | } 87 | 88 | [Test] 89 | public void ShouldUpdateVersion_MajorVersionHigher_ReturnsTrue() 90 | { 91 | // Act & Assert 92 | Assert.IsTrue( 93 | condition: Installer.ShouldUpdateVersion( 94 | currentVersion: "1.5.0", 95 | installerVersion: "2.0.0" 96 | ), 97 | message: "Should update when major version is higher" 98 | ); 99 | } 100 | 101 | [Test] 102 | public void ShouldUpdateVersion_MajorVersionLower_ReturnsFalse() 103 | { 104 | // Act & Assert 105 | Assert.IsFalse( 106 | condition: Installer.ShouldUpdateVersion( 107 | currentVersion: "2.0.0", 108 | installerVersion: "1.5.0" 109 | ), 110 | message: "Should not downgrade when major version is lower" 111 | ); 112 | } 113 | 114 | [Test] 115 | public void ShouldUpdateVersion_SameVersion_ReturnsFalse() 116 | { 117 | // Act & Assert 118 | Assert.IsFalse( 119 | condition: Installer.ShouldUpdateVersion( 120 | currentVersion: "1.5.2", 121 | installerVersion: "1.5.2" 122 | ), 123 | message: "Should not update when versions are the same" 124 | ); 125 | } 126 | 127 | [Test] 128 | public void ShouldUpdateVersion_EmptyCurrentVersion_ReturnsTrue() 129 | { 130 | // Act & Assert 131 | Assert.IsTrue( 132 | condition: Installer.ShouldUpdateVersion( 133 | currentVersion: "", 134 | installerVersion: "1.5.2" 135 | ), 136 | message: "Should install when no current version exists" 137 | ); 138 | } 139 | 140 | [Test] 141 | public void ShouldUpdateVersion_NullCurrentVersion_ReturnsTrue() 142 | { 143 | // Act & Assert 144 | Assert.IsTrue( 145 | condition: Installer.ShouldUpdateVersion( 146 | currentVersion: null, 147 | installerVersion: "1.5.2" 148 | ), 149 | message: "Should install when current version is null" 150 | ); 151 | } 152 | 153 | [Test] 154 | public void AddScopedRegistryIfNeeded_PreventVersionDowngrade_Integration() 155 | { 156 | // Arrange - Create manifest with higher version 157 | var versionParts = Installer.Version.Split('.'); 158 | var majorVersion = int.Parse(versionParts[0]); 159 | var higherVersion = $"{majorVersion + 10}.0.0"; 160 | var manifest = new JSONObject 161 | { 162 | [Installer.Dependencies] = new JSONObject 163 | { 164 | [PackageId] = higherVersion 165 | }, 166 | [Installer.ScopedRegistries] = new JSONArray() 167 | }; 168 | File.WriteAllText(TestManifestPath, manifest.ToString(2)); 169 | 170 | // Act - Run installer (should NOT downgrade) 171 | Installer.AddScopedRegistryIfNeeded(TestManifestPath); 172 | 173 | // Assert - Version should remain unchanged 174 | var updatedContent = File.ReadAllText(TestManifestPath); 175 | var updatedManifest = JSONObject.Parse(updatedContent); 176 | var actualVersion = updatedManifest[Installer.Dependencies][PackageId]; 177 | 178 | Assert.AreEqual(higherVersion, actualVersion.ToString().Trim('"'), 179 | "Version should not be downgraded from higher version"); 180 | } 181 | 182 | [Test] 183 | public void AddScopedRegistryIfNeeded_AllowVersionUpgrade_Integration() 184 | { 185 | // Arrange - Create manifest with lower version (0.0.1 is always lower) 186 | var lowerVersion = "0.0.1"; 187 | var manifest = new JSONObject 188 | { 189 | [Installer.Dependencies] = new JSONObject 190 | { 191 | [PackageId] = lowerVersion 192 | }, 193 | [Installer.ScopedRegistries] = new JSONArray() 194 | }; 195 | File.WriteAllText(TestManifestPath, manifest.ToString(2)); 196 | 197 | // Act - Run installer (should upgrade) 198 | Installer.AddScopedRegistryIfNeeded(TestManifestPath); 199 | 200 | // Assert - Version should be upgraded to installer version 201 | var updatedContent = File.ReadAllText(TestManifestPath); 202 | var updatedManifest = JSONObject.Parse(updatedContent); 203 | var actualVersion = updatedManifest[Installer.Dependencies][PackageId]; 204 | 205 | Assert.AreEqual(Installer.Version, actualVersion.ToString().Trim('"'), 206 | "Version should be upgraded to installer version"); 207 | } 208 | 209 | [Test] 210 | public void AddScopedRegistryIfNeeded_NoExistingDependency_InstallsNewVersion() 211 | { 212 | // Arrange - Create manifest without the package 213 | var manifest = new JSONObject 214 | { 215 | [Installer.Dependencies] = new JSONObject(), 216 | [Installer.ScopedRegistries] = new JSONArray() 217 | }; 218 | File.WriteAllText(TestManifestPath, manifest.ToString(2)); 219 | 220 | // Act - Run installer 221 | Installer.AddScopedRegistryIfNeeded(TestManifestPath); 222 | 223 | // Assert - Package should be added with installer version 224 | var updatedContent = File.ReadAllText(TestManifestPath); 225 | var updatedManifest = JSONObject.Parse(updatedContent); 226 | var actualVersion = updatedManifest[Installer.Dependencies][PackageId]; 227 | 228 | Assert.AreEqual(Installer.Version, actualVersion.ToString().Trim('"'), 229 | "New package should be installed with installer version"); 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml-sample: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | INSTALLER_UNITY_VERSION: 2022.3.61f1 11 | 12 | jobs: 13 | check-version-tag: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | version: ${{ steps.get_version.outputs.current-version }} 17 | prev_tag: ${{ steps.prev_tag.outputs.tag }} 18 | tag_exists: ${{ steps.tag_exists.outputs.exists }} 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v6 22 | with: 23 | fetch-depth: 0 24 | fetch-tags: true 25 | 26 | - name: Get version from package.json 27 | id: get_version 28 | uses: martinbeentjes/npm-get-version-action@v1.3.1 29 | with: 30 | path: Unity-Package/Assets/root 31 | 32 | - name: Find previous version tag 33 | id: prev_tag 34 | uses: WyriHaximus/github-action-get-previous-tag@v1 35 | 36 | - name: Check if tag exists 37 | id: tag_exists 38 | uses: mukunku/tag-exists-action@v1.6.0 39 | with: 40 | tag: ${{ steps.get_version.outputs.current-version }} 41 | 42 | build-unity-installer: 43 | runs-on: ubuntu-latest 44 | needs: [check-version-tag] 45 | if: needs.check-version-tag.outputs.tag_exists == 'false' 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v6 49 | 50 | - name: Cache Unity Library 51 | uses: actions/cache@v4 52 | with: 53 | path: ./Installer/Library 54 | key: Library-Unity-Installer 55 | 56 | - name: Test Unity Installer (EditMode) 57 | uses: game-ci/unity-test-runner@v4 58 | env: 59 | UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} 60 | UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} 61 | UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} 62 | with: 63 | projectPath: ./Installer 64 | unityVersion: ${{ env.INSTALLER_UNITY_VERSION }} 65 | customImage: "unityci/editor:ubuntu-${{ env.INSTALLER_UNITY_VERSION }}-base-3" 66 | testMode: editmode 67 | githubToken: ${{ secrets.GITHUB_TOKEN }} 68 | checkName: Unity Installer EditMode Test Results 69 | artifactsPath: artifacts-installer-editmode 70 | customParameters: -CI true -GITHUB_ACTIONS true 71 | 72 | - name: Clean Unity artifacts and reset git state 73 | run: | 74 | # Force remove Unity generated files with restricted permissions 75 | sudo rm -rf ./Installer/Logs/ || true 76 | sudo rm -rf ./Installer/Temp/ || true 77 | sudo rm -rf ./artifacts-installer-editmode/ || true 78 | 79 | # Reset only tracked files to their committed state 80 | git reset --hard HEAD 81 | echo "Cleaned Unity artifacts and reset tracked files" 82 | 83 | - name: Export Unity Package 84 | uses: game-ci/unity-builder@v4 85 | env: 86 | UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} 87 | UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} 88 | UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} 89 | with: 90 | projectPath: ./Installer 91 | unityVersion: ${{ env.INSTALLER_UNITY_VERSION }} 92 | customImage: "unityci/editor:ubuntu-${{ env.INSTALLER_UNITY_VERSION }}-base-3" 93 | buildName: Unity-Installer 94 | buildsPath: build 95 | buildMethod: YOUR_PACKAGE_ID.Installer.PackageExporter.ExportPackage 96 | customParameters: -CI true -GITHUB_ACTIONS true 97 | 98 | - name: Upload Unity Package as artifact 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: unity-installer-package 102 | path: ./Installer/build/YOUR_PACKAGE_NAME_INSTALLER_FILE.unitypackage 103 | 104 | # --- UNITY TESTS --- 105 | # ------------------- 106 | 107 | # --- EDIT MODE --- 108 | 109 | test-unity-2022-3-61f1-editmode: 110 | needs: [build-unity-installer] 111 | uses: ./.github/workflows/test_unity_plugin.yml 112 | with: 113 | projectPath: "./Unity-Package" 114 | unityVersion: "2022.3.61f1" 115 | testMode: "editmode" 116 | secrets: inherit 117 | 118 | test-unity-2023-2-20f1-editmode: 119 | needs: [build-unity-installer] 120 | uses: ./.github/workflows/test_unity_plugin.yml 121 | with: 122 | projectPath: "./Unity-Package" 123 | unityVersion: "2023.2.20f1" 124 | testMode: "editmode" 125 | secrets: inherit 126 | 127 | test-unity-6000-2-3f1-editmode: 128 | needs: [build-unity-installer] 129 | uses: ./.github/workflows/test_unity_plugin.yml 130 | with: 131 | projectPath: "./Unity-Package" 132 | unityVersion: "6000.2.3f1" 133 | testMode: "editmode" 134 | secrets: inherit 135 | 136 | # --- PLAY MODE --- 137 | 138 | test-unity-2022-3-61f1-playmode: 139 | needs: [build-unity-installer] 140 | uses: ./.github/workflows/test_unity_plugin.yml 141 | with: 142 | projectPath: "./Unity-Package" 143 | unityVersion: "2022.3.61f1" 144 | testMode: "playmode" 145 | secrets: inherit 146 | 147 | test-unity-2023-2-20f1-playmode: 148 | needs: [build-unity-installer] 149 | uses: ./.github/workflows/test_unity_plugin.yml 150 | with: 151 | projectPath: "./Unity-Package" 152 | unityVersion: "2023.2.20f1" 153 | testMode: "playmode" 154 | secrets: inherit 155 | 156 | test-unity-6000-2-3f1-playmode: 157 | needs: [build-unity-installer] 158 | uses: ./.github/workflows/test_unity_plugin.yml 159 | with: 160 | projectPath: "./Unity-Package" 161 | unityVersion: "6000.2.3f1" 162 | testMode: "playmode" 163 | secrets: inherit 164 | 165 | # --- STANDALONE --- 166 | 167 | test-unity-2022-3-61f1-standalone: 168 | needs: [build-unity-installer] 169 | uses: ./.github/workflows/test_unity_plugin.yml 170 | with: 171 | projectPath: "./Unity-Package" 172 | unityVersion: "2022.3.61f1" 173 | testMode: "standalone" 174 | secrets: inherit 175 | 176 | test-unity-2023-2-20f1-standalone: 177 | needs: [build-unity-installer] 178 | uses: ./.github/workflows/test_unity_plugin.yml 179 | with: 180 | projectPath: "./Unity-Package" 181 | unityVersion: "2023.2.20f1" 182 | testMode: "standalone" 183 | secrets: inherit 184 | 185 | test-unity-6000-2-3f1-standalone: 186 | needs: [build-unity-installer] 187 | uses: ./.github/workflows/test_unity_plugin.yml 188 | with: 189 | projectPath: "./Unity-Package" 190 | unityVersion: "6000.2.3f1" 191 | testMode: "standalone" 192 | secrets: inherit 193 | 194 | # ------------------- 195 | 196 | release-unity-plugin: 197 | runs-on: ubuntu-latest 198 | needs: 199 | [ 200 | check-version-tag, 201 | build-unity-installer, 202 | test-unity-2022-3-61f1-editmode, 203 | test-unity-2022-3-61f1-playmode, 204 | test-unity-2022-3-61f1-standalone, 205 | test-unity-2023-2-20f1-editmode, 206 | test-unity-2023-2-20f1-playmode, 207 | test-unity-2023-2-20f1-standalone, 208 | test-unity-6000-2-3f1-editmode, 209 | test-unity-6000-2-3f1-playmode, 210 | test-unity-6000-2-3f1-standalone, 211 | ] 212 | if: needs.check-version-tag.outputs.tag_exists == 'false' 213 | outputs: 214 | version: ${{ needs.check-version-tag.outputs.version }} 215 | success: ${{ steps.rel_desc.outputs.success }} 216 | release_notes: ${{ steps.rel_desc.outputs.release_body }} 217 | steps: 218 | - name: Checkout repository 219 | uses: actions/checkout@v6 220 | with: 221 | fetch-depth: 0 222 | fetch-tags: true 223 | 224 | - name: Generate release description 225 | id: rel_desc 226 | env: 227 | GH_TOKEN: ${{ github.token }} 228 | run: | 229 | set -e 230 | version=${{ needs.check-version-tag.outputs.version }} 231 | prev_tag=${{ needs.check-version-tag.outputs.prev_tag }} 232 | repo_url="https://github.com/${GITHUB_REPOSITORY}" 233 | today=$(date +'%B %e, %Y') 234 | 235 | echo "repo_url: $repo_url" 236 | echo "today: $today" 237 | 238 | echo "# Package $version" > release.md 239 | echo "**Released:** *$today*" >> release.md 240 | 241 | echo "" >> release.md 242 | echo "---" >> release.md 243 | echo "" >> release.md 244 | 245 | if [ -n "$prev_tag" ]; then 246 | echo "## Comparison" >> release.md 247 | echo "See every change: [Compare $prev_tag...$version]($repo_url/compare/$prev_tag...$version)" >> release.md 248 | 249 | echo "" >> release.md 250 | echo "---" >> release.md 251 | echo "" >> release.md 252 | 253 | echo "## Commit Summary (Newest → Oldest)" >> release.md 254 | for sha in $(git log --pretty=format:'%H' $prev_tag..HEAD); do 255 | username=$(gh api repos/${GITHUB_REPOSITORY}/commits/$sha --jq '.author.login // .commit.author.name') 256 | message=$(git log -1 --pretty=format:'%s' $sha) 257 | short_sha=$(git log -1 --pretty=format:'%h' $sha) 258 | echo "- [\`$short_sha\`]($repo_url/commit/$sha) — $message by @$username" >> release.md 259 | done 260 | fi 261 | 262 | printf "release_body<> $GITHUB_OUTPUT 263 | echo "success=true" >> $GITHUB_OUTPUT 264 | 265 | - name: Create Tag and Release 266 | uses: softprops/action-gh-release@v2 267 | with: 268 | tag_name: ${{ needs.check-version-tag.outputs.version }} 269 | name: ${{ needs.check-version-tag.outputs.version }} 270 | body: ${{ steps.rel_desc.outputs.release_body }} 271 | draft: false 272 | prerelease: false 273 | env: 274 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 275 | 276 | publish-unity-installer: 277 | runs-on: ubuntu-latest 278 | needs: release-unity-plugin 279 | if: needs.release-unity-plugin.outputs.success == 'true' 280 | steps: 281 | - name: Download Unity Package artifact 282 | uses: actions/download-artifact@v4 283 | with: 284 | name: unity-installer-package 285 | path: ./ 286 | 287 | - name: Upload Unity Package to Release 288 | uses: softprops/action-gh-release@v2 289 | with: 290 | files: ./Unity-Installer.unitypackage 291 | tag_name: ${{ needs.release-unity-plugin.outputs.version }} 292 | env: 293 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 294 | 295 | # Cleanup job to remove build artifacts after publishing 296 | cleanup-artifacts: 297 | runs-on: ubuntu-latest 298 | needs: [publish-unity-installer] 299 | if: always() 300 | steps: 301 | - name: Delete Unity Package artifacts 302 | uses: geekyeggo/delete-artifact@v5 303 | with: 304 | name: unity-installer-package 305 | failOnError: false 306 | continue-on-error: true 307 | -------------------------------------------------------------------------------- /Installer/Assets/YOUR_PACKAGE_TITLE/SimpleJSON.cs: -------------------------------------------------------------------------------- 1 | /* * * * * 2 | * A simple JSON Parser / builder 3 | * ------------------------------ 4 | * 5 | * It mainly has been written as a simple JSON parser. It can build a JSON string 6 | * from the node-tree, or generate a node tree from any valid JSON string. 7 | * 8 | * Written by Bunny83 9 | * 2012-06-09 10 | * 11 | * Changelog now external. See Changelog.txt 12 | * 13 | * The MIT License (MIT) 14 | * 15 | * Copyright (c) 2012-2022 Markus Göbel (Bunny83) 16 | * 17 | * Permission is hereby granted, free of charge, to any person obtaining a copy 18 | * of this software and associated documentation files (the "Software"), to deal 19 | * in the Software without restriction, including without limitation the rights 20 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | * copies of the Software, and to permit persons to whom the Software is 22 | * furnished to do so, subject to the following conditions: 23 | * 24 | * The above copyright notice and this permission notice shall be included in all 25 | * copies or substantial portions of the Software. 26 | * 27 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | * SOFTWARE. 34 | * 35 | * * * * */ 36 | using System; 37 | using System.Collections; 38 | using System.Collections.Generic; 39 | using System.Globalization; 40 | using System.Linq; 41 | using System.Text; 42 | 43 | namespace YOUR_PACKAGE_ID.Installer.SimpleJSON 44 | { 45 | public enum JSONNodeType 46 | { 47 | Array = 1, 48 | Object = 2, 49 | String = 3, 50 | Number = 4, 51 | NullValue = 5, 52 | Boolean = 6, 53 | None = 7, 54 | Custom = 0xFF, 55 | } 56 | public enum JSONTextMode 57 | { 58 | Compact, 59 | Indent 60 | } 61 | 62 | public abstract partial class JSONNode 63 | { 64 | #region Enumerators 65 | public struct Enumerator 66 | { 67 | private enum Type { None, Array, Object } 68 | private Type type; 69 | private Dictionary.Enumerator m_Object; 70 | private List.Enumerator m_Array; 71 | public bool IsValid { get { return type != Type.None; } } 72 | public Enumerator(List.Enumerator aArrayEnum) 73 | { 74 | type = Type.Array; 75 | m_Object = default(Dictionary.Enumerator); 76 | m_Array = aArrayEnum; 77 | } 78 | public Enumerator(Dictionary.Enumerator aDictEnum) 79 | { 80 | type = Type.Object; 81 | m_Object = aDictEnum; 82 | m_Array = default(List.Enumerator); 83 | } 84 | public KeyValuePair Current 85 | { 86 | get 87 | { 88 | if (type == Type.Array) 89 | return new KeyValuePair(string.Empty, m_Array.Current); 90 | else if (type == Type.Object) 91 | return m_Object.Current; 92 | return new KeyValuePair(string.Empty, null); 93 | } 94 | } 95 | public bool MoveNext() 96 | { 97 | if (type == Type.Array) 98 | return m_Array.MoveNext(); 99 | else if (type == Type.Object) 100 | return m_Object.MoveNext(); 101 | return false; 102 | } 103 | } 104 | public struct ValueEnumerator 105 | { 106 | private Enumerator m_Enumerator; 107 | public ValueEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } 108 | public ValueEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } 109 | public ValueEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } 110 | public JSONNode Current { get { return m_Enumerator.Current.Value; } } 111 | public bool MoveNext() { return m_Enumerator.MoveNext(); } 112 | public ValueEnumerator GetEnumerator() { return this; } 113 | } 114 | public struct KeyEnumerator 115 | { 116 | private Enumerator m_Enumerator; 117 | public KeyEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { } 118 | public KeyEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { } 119 | public KeyEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; } 120 | public string Current { get { return m_Enumerator.Current.Key; } } 121 | public bool MoveNext() { return m_Enumerator.MoveNext(); } 122 | public KeyEnumerator GetEnumerator() { return this; } 123 | } 124 | 125 | public class LinqEnumerator : IEnumerator>, IEnumerable> 126 | { 127 | private JSONNode m_Node; 128 | private Enumerator m_Enumerator; 129 | internal LinqEnumerator(JSONNode aNode) 130 | { 131 | m_Node = aNode; 132 | if (m_Node != null) 133 | m_Enumerator = m_Node.GetEnumerator(); 134 | } 135 | public KeyValuePair Current { get { return m_Enumerator.Current; } } 136 | object IEnumerator.Current { get { return m_Enumerator.Current; } } 137 | public bool MoveNext() { return m_Enumerator.MoveNext(); } 138 | 139 | public void Dispose() 140 | { 141 | m_Node = null; 142 | m_Enumerator = new Enumerator(); 143 | } 144 | 145 | public IEnumerator> GetEnumerator() 146 | { 147 | return new LinqEnumerator(m_Node); 148 | } 149 | 150 | public void Reset() 151 | { 152 | if (m_Node != null) 153 | m_Enumerator = m_Node.GetEnumerator(); 154 | } 155 | 156 | IEnumerator IEnumerable.GetEnumerator() 157 | { 158 | return new LinqEnumerator(m_Node); 159 | } 160 | } 161 | 162 | #endregion Enumerators 163 | 164 | #region common interface 165 | 166 | public static bool forceASCII = false; // Use Unicode by default 167 | public static bool longAsString = false; // lazy creator creates a JSONString instead of JSONNumber 168 | public static bool allowLineComments = true; // allow "//"-style comments at the end of a line 169 | 170 | public abstract JSONNodeType Tag { get; } 171 | 172 | public virtual JSONNode this[int aIndex] { get { return null; } set { } } 173 | 174 | public virtual JSONNode this[string aKey] { get { return null; } set { } } 175 | 176 | public virtual string Value { get { return ""; } set { } } 177 | 178 | public virtual int Count { get { return 0; } } 179 | 180 | public virtual bool IsNumber { get { return false; } } 181 | public virtual bool IsString { get { return false; } } 182 | public virtual bool IsBoolean { get { return false; } } 183 | public virtual bool IsNull { get { return false; } } 184 | public virtual bool IsArray { get { return false; } } 185 | public virtual bool IsObject { get { return false; } } 186 | 187 | public virtual bool Inline { get { return false; } set { } } 188 | 189 | public virtual void Add(string aKey, JSONNode aItem) 190 | { 191 | } 192 | public virtual void Add(JSONNode aItem) 193 | { 194 | Add("", aItem); 195 | } 196 | 197 | public virtual JSONNode Remove(string aKey) 198 | { 199 | return null; 200 | } 201 | 202 | public virtual JSONNode Remove(int aIndex) 203 | { 204 | return null; 205 | } 206 | 207 | public virtual JSONNode Remove(JSONNode aNode) 208 | { 209 | return aNode; 210 | } 211 | public virtual void Clear() { } 212 | 213 | public virtual JSONNode Clone() 214 | { 215 | return null; 216 | } 217 | 218 | public virtual IEnumerable Children 219 | { 220 | get 221 | { 222 | yield break; 223 | } 224 | } 225 | 226 | public IEnumerable DeepChildren 227 | { 228 | get 229 | { 230 | foreach (var C in Children) 231 | foreach (var D in C.DeepChildren) 232 | yield return D; 233 | } 234 | } 235 | 236 | public virtual bool HasKey(string aKey) 237 | { 238 | return false; 239 | } 240 | 241 | public virtual JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) 242 | { 243 | return aDefault; 244 | } 245 | 246 | public override string ToString() 247 | { 248 | StringBuilder sb = new StringBuilder(); 249 | WriteToStringBuilder(sb, 0, 0, JSONTextMode.Compact); 250 | return sb.ToString(); 251 | } 252 | 253 | public virtual string ToString(int aIndent) 254 | { 255 | StringBuilder sb = new StringBuilder(); 256 | WriteToStringBuilder(sb, 0, aIndent, JSONTextMode.Indent); 257 | return sb.ToString(); 258 | } 259 | internal abstract void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode); 260 | 261 | public abstract Enumerator GetEnumerator(); 262 | public IEnumerable> Linq { get { return new LinqEnumerator(this); } } 263 | public KeyEnumerator Keys { get { return new KeyEnumerator(GetEnumerator()); } } 264 | public ValueEnumerator Values { get { return new ValueEnumerator(GetEnumerator()); } } 265 | 266 | #endregion common interface 267 | 268 | #region typecasting properties 269 | 270 | 271 | public virtual double AsDouble 272 | { 273 | get 274 | { 275 | double v = 0.0; 276 | if (double.TryParse(Value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) 277 | return v; 278 | return 0.0; 279 | } 280 | set 281 | { 282 | Value = value.ToString(CultureInfo.InvariantCulture); 283 | } 284 | } 285 | 286 | public virtual int AsInt 287 | { 288 | get { return (int)AsDouble; } 289 | set { AsDouble = value; } 290 | } 291 | 292 | public virtual float AsFloat 293 | { 294 | get { return (float)AsDouble; } 295 | set { AsDouble = value; } 296 | } 297 | 298 | public virtual bool AsBool 299 | { 300 | get 301 | { 302 | bool v = false; 303 | if (bool.TryParse(Value, out v)) 304 | return v; 305 | return !string.IsNullOrEmpty(Value); 306 | } 307 | set 308 | { 309 | Value = (value) ? "true" : "false"; 310 | } 311 | } 312 | 313 | public virtual long AsLong 314 | { 315 | get 316 | { 317 | long val = 0; 318 | if (long.TryParse(Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) 319 | return val; 320 | return 0L; 321 | } 322 | set 323 | { 324 | Value = value.ToString(CultureInfo.InvariantCulture); 325 | } 326 | } 327 | 328 | public virtual ulong AsULong 329 | { 330 | get 331 | { 332 | ulong val = 0; 333 | if (ulong.TryParse(Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) 334 | return val; 335 | return 0; 336 | } 337 | set 338 | { 339 | Value = value.ToString(CultureInfo.InvariantCulture); 340 | } 341 | } 342 | 343 | public virtual JSONArray AsArray 344 | { 345 | get 346 | { 347 | return this as JSONArray; 348 | } 349 | } 350 | 351 | public virtual JSONObject AsObject 352 | { 353 | get 354 | { 355 | return this as JSONObject; 356 | } 357 | } 358 | 359 | 360 | #endregion typecasting properties 361 | 362 | #region operators 363 | 364 | public static implicit operator JSONNode(string s) 365 | { 366 | return (s == null) ? (JSONNode)JSONNull.CreateOrGet() : new JSONString(s); 367 | } 368 | public static implicit operator string(JSONNode d) 369 | { 370 | return (d == null) ? null : d.Value; 371 | } 372 | 373 | public static implicit operator JSONNode(double n) 374 | { 375 | return new JSONNumber(n); 376 | } 377 | public static implicit operator double(JSONNode d) 378 | { 379 | return (d == null) ? 0 : d.AsDouble; 380 | } 381 | 382 | public static implicit operator JSONNode(float n) 383 | { 384 | return new JSONNumber(n); 385 | } 386 | public static implicit operator float(JSONNode d) 387 | { 388 | return (d == null) ? 0 : d.AsFloat; 389 | } 390 | 391 | public static implicit operator JSONNode(int n) 392 | { 393 | return new JSONNumber(n); 394 | } 395 | public static implicit operator int(JSONNode d) 396 | { 397 | return (d == null) ? 0 : d.AsInt; 398 | } 399 | 400 | public static implicit operator JSONNode(long n) 401 | { 402 | if (longAsString) 403 | return new JSONString(n.ToString(CultureInfo.InvariantCulture)); 404 | return new JSONNumber(n); 405 | } 406 | public static implicit operator long(JSONNode d) 407 | { 408 | return (d == null) ? 0L : d.AsLong; 409 | } 410 | 411 | public static implicit operator JSONNode(ulong n) 412 | { 413 | if (longAsString) 414 | return new JSONString(n.ToString(CultureInfo.InvariantCulture)); 415 | return new JSONNumber(n); 416 | } 417 | public static implicit operator ulong(JSONNode d) 418 | { 419 | return (d == null) ? 0 : d.AsULong; 420 | } 421 | 422 | public static implicit operator JSONNode(bool b) 423 | { 424 | return new JSONBool(b); 425 | } 426 | public static implicit operator bool(JSONNode d) 427 | { 428 | return (d == null) ? false : d.AsBool; 429 | } 430 | 431 | public static implicit operator JSONNode(KeyValuePair aKeyValue) 432 | { 433 | return aKeyValue.Value; 434 | } 435 | 436 | public static bool operator ==(JSONNode a, object b) 437 | { 438 | if (ReferenceEquals(a, b)) 439 | return true; 440 | bool aIsNull = a is JSONNull || ReferenceEquals(a, null) || a is JSONLazyCreator; 441 | bool bIsNull = b is JSONNull || ReferenceEquals(b, null) || b is JSONLazyCreator; 442 | if (aIsNull && bIsNull) 443 | return true; 444 | return !aIsNull && a.Equals(b); 445 | } 446 | 447 | public static bool operator !=(JSONNode a, object b) 448 | { 449 | return !(a == b); 450 | } 451 | 452 | public override bool Equals(object obj) 453 | { 454 | return ReferenceEquals(this, obj); 455 | } 456 | 457 | public override int GetHashCode() 458 | { 459 | return base.GetHashCode(); 460 | } 461 | 462 | #endregion operators 463 | 464 | [ThreadStatic] 465 | private static StringBuilder m_EscapeBuilder; 466 | internal static StringBuilder EscapeBuilder 467 | { 468 | get 469 | { 470 | if (m_EscapeBuilder == null) 471 | m_EscapeBuilder = new StringBuilder(); 472 | return m_EscapeBuilder; 473 | } 474 | } 475 | internal static string Escape(string aText) 476 | { 477 | var sb = EscapeBuilder; 478 | sb.Length = 0; 479 | if (sb.Capacity < aText.Length + aText.Length / 10) 480 | sb.Capacity = aText.Length + aText.Length / 10; 481 | foreach (char c in aText) 482 | { 483 | switch (c) 484 | { 485 | case '\\': 486 | sb.Append("\\\\"); 487 | break; 488 | case '\"': 489 | sb.Append("\\\""); 490 | break; 491 | case '\n': 492 | sb.Append("\\n"); 493 | break; 494 | case '\r': 495 | sb.Append("\\r"); 496 | break; 497 | case '\t': 498 | sb.Append("\\t"); 499 | break; 500 | case '\b': 501 | sb.Append("\\b"); 502 | break; 503 | case '\f': 504 | sb.Append("\\f"); 505 | break; 506 | default: 507 | if (c < ' ' || (forceASCII && c > 127)) 508 | { 509 | ushort val = c; 510 | sb.Append("\\u").Append(val.ToString("X4")); 511 | } 512 | else 513 | sb.Append(c); 514 | break; 515 | } 516 | } 517 | string result = sb.ToString(); 518 | sb.Length = 0; 519 | return result; 520 | } 521 | 522 | private static JSONNode ParseElement(string token, bool quoted) 523 | { 524 | if (quoted) 525 | return token; 526 | if (token.Length <= 5) 527 | { 528 | string tmp = token.ToLower(); 529 | if (tmp == "false" || tmp == "true") 530 | return tmp == "true"; 531 | if (tmp == "null") 532 | return JSONNull.CreateOrGet(); 533 | } 534 | double val; 535 | if (double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out val)) 536 | return val; 537 | else 538 | return token; 539 | } 540 | 541 | public static JSONNode Parse(string aJSON) 542 | { 543 | Stack stack = new Stack(); 544 | JSONNode ctx = null; 545 | int i = 0; 546 | StringBuilder Token = new StringBuilder(); 547 | string TokenName = ""; 548 | bool QuoteMode = false; 549 | bool TokenIsQuoted = false; 550 | bool HasNewlineChar = false; 551 | while (i < aJSON.Length) 552 | { 553 | switch (aJSON[i]) 554 | { 555 | case '{': 556 | if (QuoteMode) 557 | { 558 | Token.Append(aJSON[i]); 559 | break; 560 | } 561 | stack.Push(new JSONObject()); 562 | if (ctx != null) 563 | { 564 | ctx.Add(TokenName, stack.Peek()); 565 | } 566 | TokenName = ""; 567 | Token.Length = 0; 568 | ctx = stack.Peek(); 569 | HasNewlineChar = false; 570 | break; 571 | 572 | case '[': 573 | if (QuoteMode) 574 | { 575 | Token.Append(aJSON[i]); 576 | break; 577 | } 578 | 579 | stack.Push(new JSONArray()); 580 | if (ctx != null) 581 | { 582 | ctx.Add(TokenName, stack.Peek()); 583 | } 584 | TokenName = ""; 585 | Token.Length = 0; 586 | ctx = stack.Peek(); 587 | HasNewlineChar = false; 588 | break; 589 | 590 | case '}': 591 | case ']': 592 | if (QuoteMode) 593 | { 594 | 595 | Token.Append(aJSON[i]); 596 | break; 597 | } 598 | if (stack.Count == 0) 599 | throw new Exception("JSON Parse: Too many closing brackets"); 600 | 601 | stack.Pop(); 602 | if (Token.Length > 0 || TokenIsQuoted) 603 | ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); 604 | if (ctx != null) 605 | ctx.Inline = !HasNewlineChar; 606 | TokenIsQuoted = false; 607 | TokenName = ""; 608 | Token.Length = 0; 609 | if (stack.Count > 0) 610 | ctx = stack.Peek(); 611 | break; 612 | 613 | case ':': 614 | if (QuoteMode) 615 | { 616 | Token.Append(aJSON[i]); 617 | break; 618 | } 619 | TokenName = Token.ToString(); 620 | Token.Length = 0; 621 | TokenIsQuoted = false; 622 | break; 623 | 624 | case '"': 625 | QuoteMode ^= true; 626 | TokenIsQuoted |= QuoteMode; 627 | break; 628 | 629 | case ',': 630 | if (QuoteMode) 631 | { 632 | Token.Append(aJSON[i]); 633 | break; 634 | } 635 | if (Token.Length > 0 || TokenIsQuoted) 636 | ctx.Add(TokenName, ParseElement(Token.ToString(), TokenIsQuoted)); 637 | TokenIsQuoted = false; 638 | TokenName = ""; 639 | Token.Length = 0; 640 | TokenIsQuoted = false; 641 | break; 642 | 643 | case '\r': 644 | case '\n': 645 | HasNewlineChar = true; 646 | break; 647 | 648 | case ' ': 649 | case '\t': 650 | if (QuoteMode) 651 | Token.Append(aJSON[i]); 652 | break; 653 | 654 | case '\\': 655 | ++i; 656 | if (QuoteMode) 657 | { 658 | char C = aJSON[i]; 659 | switch (C) 660 | { 661 | case 't': 662 | Token.Append('\t'); 663 | break; 664 | case 'r': 665 | Token.Append('\r'); 666 | break; 667 | case 'n': 668 | Token.Append('\n'); 669 | break; 670 | case 'b': 671 | Token.Append('\b'); 672 | break; 673 | case 'f': 674 | Token.Append('\f'); 675 | break; 676 | case 'u': 677 | { 678 | string s = aJSON.Substring(i + 1, 4); 679 | Token.Append((char)int.Parse( 680 | s, 681 | System.Globalization.NumberStyles.AllowHexSpecifier)); 682 | i += 4; 683 | break; 684 | } 685 | default: 686 | Token.Append(C); 687 | break; 688 | } 689 | } 690 | break; 691 | case '/': 692 | if (allowLineComments && !QuoteMode && i + 1 < aJSON.Length && aJSON[i + 1] == '/') 693 | { 694 | while (++i < aJSON.Length && aJSON[i] != '\n' && aJSON[i] != '\r') ; 695 | break; 696 | } 697 | Token.Append(aJSON[i]); 698 | break; 699 | case '\uFEFF': // remove / ignore BOM (Byte Order Mark) 700 | break; 701 | 702 | default: 703 | Token.Append(aJSON[i]); 704 | break; 705 | } 706 | ++i; 707 | } 708 | if (QuoteMode) 709 | { 710 | throw new Exception("JSON Parse: Quotation marks seems to be messed up."); 711 | } 712 | if (ctx == null) 713 | return ParseElement(Token.ToString(), TokenIsQuoted); 714 | return ctx; 715 | } 716 | 717 | } 718 | // End of JSONNode 719 | 720 | public partial class JSONArray : JSONNode 721 | { 722 | private List m_List = new List(); 723 | private bool inline = false; 724 | public override bool Inline 725 | { 726 | get { return inline; } 727 | set { inline = value; } 728 | } 729 | 730 | public override JSONNodeType Tag { get { return JSONNodeType.Array; } } 731 | public override bool IsArray { get { return true; } } 732 | public override Enumerator GetEnumerator() { return new Enumerator(m_List.GetEnumerator()); } 733 | 734 | public override JSONNode this[int aIndex] 735 | { 736 | get 737 | { 738 | if (aIndex < 0 || aIndex >= m_List.Count) 739 | return new JSONLazyCreator(this); 740 | return m_List[aIndex]; 741 | } 742 | set 743 | { 744 | if (value == null) 745 | value = JSONNull.CreateOrGet(); 746 | if (aIndex < 0 || aIndex >= m_List.Count) 747 | m_List.Add(value); 748 | else 749 | m_List[aIndex] = value; 750 | } 751 | } 752 | 753 | public override JSONNode this[string aKey] 754 | { 755 | get { return new JSONLazyCreator(this); } 756 | set 757 | { 758 | if (value == null) 759 | value = JSONNull.CreateOrGet(); 760 | m_List.Add(value); 761 | } 762 | } 763 | 764 | public override int Count 765 | { 766 | get { return m_List.Count; } 767 | } 768 | 769 | public override void Add(string aKey, JSONNode aItem) 770 | { 771 | if (aItem == null) 772 | aItem = JSONNull.CreateOrGet(); 773 | m_List.Add(aItem); 774 | } 775 | 776 | public override JSONNode Remove(int aIndex) 777 | { 778 | if (aIndex < 0 || aIndex >= m_List.Count) 779 | return null; 780 | JSONNode tmp = m_List[aIndex]; 781 | m_List.RemoveAt(aIndex); 782 | return tmp; 783 | } 784 | 785 | public override JSONNode Remove(JSONNode aNode) 786 | { 787 | m_List.Remove(aNode); 788 | return aNode; 789 | } 790 | 791 | public override void Clear() 792 | { 793 | m_List.Clear(); 794 | } 795 | 796 | public override JSONNode Clone() 797 | { 798 | var node = new JSONArray(); 799 | node.m_List.Capacity = m_List.Capacity; 800 | foreach (var n in m_List) 801 | { 802 | if (n != null) 803 | node.Add(n.Clone()); 804 | else 805 | node.Add(null); 806 | } 807 | return node; 808 | } 809 | 810 | public override IEnumerable Children 811 | { 812 | get 813 | { 814 | foreach (JSONNode N in m_List) 815 | yield return N; 816 | } 817 | } 818 | 819 | 820 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 821 | { 822 | aSB.Append('['); 823 | int count = m_List.Count; 824 | if (inline) 825 | aMode = JSONTextMode.Compact; 826 | for (int i = 0; i < count; i++) 827 | { 828 | if (i > 0) 829 | aSB.Append(','); 830 | if (aMode == JSONTextMode.Indent) 831 | aSB.AppendLine(); 832 | 833 | if (aMode == JSONTextMode.Indent) 834 | aSB.Append(' ', aIndent + aIndentInc); 835 | m_List[i].WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); 836 | } 837 | if (aMode == JSONTextMode.Indent) 838 | aSB.AppendLine().Append(' ', aIndent); 839 | aSB.Append(']'); 840 | } 841 | } 842 | // End of JSONArray 843 | 844 | public partial class JSONObject : JSONNode 845 | { 846 | private Dictionary m_Dict = new Dictionary(); 847 | 848 | private bool inline = false; 849 | public override bool Inline 850 | { 851 | get { return inline; } 852 | set { inline = value; } 853 | } 854 | 855 | public override JSONNodeType Tag { get { return JSONNodeType.Object; } } 856 | public override bool IsObject { get { return true; } } 857 | 858 | public override Enumerator GetEnumerator() { return new Enumerator(m_Dict.GetEnumerator()); } 859 | 860 | 861 | public override JSONNode this[string aKey] 862 | { 863 | get 864 | { 865 | if (m_Dict.TryGetValue(aKey, out JSONNode outJsonNode)) 866 | return outJsonNode; 867 | else 868 | return new JSONLazyCreator(this, aKey); 869 | } 870 | set 871 | { 872 | if (value == null) 873 | value = JSONNull.CreateOrGet(); 874 | if (m_Dict.ContainsKey(aKey)) 875 | m_Dict[aKey] = value; 876 | else 877 | m_Dict.Add(aKey, value); 878 | } 879 | } 880 | 881 | public override JSONNode this[int aIndex] 882 | { 883 | get 884 | { 885 | if (aIndex < 0 || aIndex >= m_Dict.Count) 886 | return null; 887 | return m_Dict.ElementAt(aIndex).Value; 888 | } 889 | set 890 | { 891 | if (value == null) 892 | value = JSONNull.CreateOrGet(); 893 | if (aIndex < 0 || aIndex >= m_Dict.Count) 894 | return; 895 | string key = m_Dict.ElementAt(aIndex).Key; 896 | m_Dict[key] = value; 897 | } 898 | } 899 | 900 | public override int Count 901 | { 902 | get { return m_Dict.Count; } 903 | } 904 | 905 | public override void Add(string aKey, JSONNode aItem) 906 | { 907 | if (aItem == null) 908 | aItem = JSONNull.CreateOrGet(); 909 | 910 | if (aKey != null) 911 | { 912 | if (m_Dict.ContainsKey(aKey)) 913 | m_Dict[aKey] = aItem; 914 | else 915 | m_Dict.Add(aKey, aItem); 916 | } 917 | else 918 | m_Dict.Add(Guid.NewGuid().ToString(), aItem); 919 | } 920 | 921 | public override JSONNode Remove(string aKey) 922 | { 923 | if (!m_Dict.ContainsKey(aKey)) 924 | return null; 925 | JSONNode tmp = m_Dict[aKey]; 926 | m_Dict.Remove(aKey); 927 | return tmp; 928 | } 929 | 930 | public override JSONNode Remove(int aIndex) 931 | { 932 | if (aIndex < 0 || aIndex >= m_Dict.Count) 933 | return null; 934 | var item = m_Dict.ElementAt(aIndex); 935 | m_Dict.Remove(item.Key); 936 | return item.Value; 937 | } 938 | 939 | public override JSONNode Remove(JSONNode aNode) 940 | { 941 | try 942 | { 943 | var item = m_Dict.Where(k => k.Value == aNode).First(); 944 | m_Dict.Remove(item.Key); 945 | return aNode; 946 | } 947 | catch 948 | { 949 | return null; 950 | } 951 | } 952 | 953 | public override void Clear() 954 | { 955 | m_Dict.Clear(); 956 | } 957 | 958 | public override JSONNode Clone() 959 | { 960 | var node = new JSONObject(); 961 | foreach (var n in m_Dict) 962 | { 963 | node.Add(n.Key, n.Value.Clone()); 964 | } 965 | return node; 966 | } 967 | 968 | public override bool HasKey(string aKey) 969 | { 970 | return m_Dict.ContainsKey(aKey); 971 | } 972 | 973 | public override JSONNode GetValueOrDefault(string aKey, JSONNode aDefault) 974 | { 975 | JSONNode res; 976 | if (m_Dict.TryGetValue(aKey, out res)) 977 | return res; 978 | return aDefault; 979 | } 980 | 981 | public override IEnumerable Children 982 | { 983 | get 984 | { 985 | foreach (KeyValuePair N in m_Dict) 986 | yield return N.Value; 987 | } 988 | } 989 | 990 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 991 | { 992 | aSB.Append('{'); 993 | bool first = true; 994 | if (inline) 995 | aMode = JSONTextMode.Compact; 996 | foreach (var k in m_Dict) 997 | { 998 | if (!first) 999 | aSB.Append(','); 1000 | first = false; 1001 | if (aMode == JSONTextMode.Indent) 1002 | aSB.AppendLine(); 1003 | if (aMode == JSONTextMode.Indent) 1004 | aSB.Append(' ', aIndent + aIndentInc); 1005 | aSB.Append('\"').Append(Escape(k.Key)).Append('\"'); 1006 | if (aMode == JSONTextMode.Compact) 1007 | aSB.Append(':'); 1008 | else 1009 | aSB.Append(" : "); 1010 | k.Value.WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode); 1011 | } 1012 | if (aMode == JSONTextMode.Indent) 1013 | aSB.AppendLine().Append(' ', aIndent); 1014 | aSB.Append('}'); 1015 | } 1016 | 1017 | } 1018 | // End of JSONObject 1019 | 1020 | public partial class JSONString : JSONNode 1021 | { 1022 | private string m_Data; 1023 | 1024 | public override JSONNodeType Tag { get { return JSONNodeType.String; } } 1025 | public override bool IsString { get { return true; } } 1026 | 1027 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1028 | 1029 | 1030 | public override string Value 1031 | { 1032 | get { return m_Data; } 1033 | set 1034 | { 1035 | m_Data = value; 1036 | } 1037 | } 1038 | 1039 | public JSONString(string aData) 1040 | { 1041 | m_Data = aData; 1042 | } 1043 | public override JSONNode Clone() 1044 | { 1045 | return new JSONString(m_Data); 1046 | } 1047 | 1048 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1049 | { 1050 | aSB.Append('\"').Append(Escape(m_Data)).Append('\"'); 1051 | } 1052 | public override bool Equals(object obj) 1053 | { 1054 | if (base.Equals(obj)) 1055 | return true; 1056 | string s = obj as string; 1057 | if (s != null) 1058 | return m_Data == s; 1059 | JSONString s2 = obj as JSONString; 1060 | if (s2 != null) 1061 | return m_Data == s2.m_Data; 1062 | return false; 1063 | } 1064 | public override int GetHashCode() 1065 | { 1066 | return m_Data.GetHashCode(); 1067 | } 1068 | public override void Clear() 1069 | { 1070 | m_Data = ""; 1071 | } 1072 | } 1073 | // End of JSONString 1074 | 1075 | public partial class JSONNumber : JSONNode 1076 | { 1077 | private double m_Data; 1078 | 1079 | public override JSONNodeType Tag { get { return JSONNodeType.Number; } } 1080 | public override bool IsNumber { get { return true; } } 1081 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1082 | 1083 | public override string Value 1084 | { 1085 | get { return m_Data.ToString(CultureInfo.InvariantCulture); } 1086 | set 1087 | { 1088 | double v; 1089 | if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out v)) 1090 | m_Data = v; 1091 | } 1092 | } 1093 | 1094 | public override double AsDouble 1095 | { 1096 | get { return m_Data; } 1097 | set { m_Data = value; } 1098 | } 1099 | public override long AsLong 1100 | { 1101 | get { return (long)m_Data; } 1102 | set { m_Data = value; } 1103 | } 1104 | public override ulong AsULong 1105 | { 1106 | get { return (ulong)m_Data; } 1107 | set { m_Data = value; } 1108 | } 1109 | 1110 | public JSONNumber(double aData) 1111 | { 1112 | m_Data = aData; 1113 | } 1114 | 1115 | public JSONNumber(string aData) 1116 | { 1117 | Value = aData; 1118 | } 1119 | 1120 | public override JSONNode Clone() 1121 | { 1122 | return new JSONNumber(m_Data); 1123 | } 1124 | 1125 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1126 | { 1127 | aSB.Append(Value.ToString(CultureInfo.InvariantCulture)); 1128 | } 1129 | private static bool IsNumeric(object value) 1130 | { 1131 | return value is int || value is uint 1132 | || value is float || value is double 1133 | || value is decimal 1134 | || value is long || value is ulong 1135 | || value is short || value is ushort 1136 | || value is sbyte || value is byte; 1137 | } 1138 | public override bool Equals(object obj) 1139 | { 1140 | if (obj == null) 1141 | return false; 1142 | if (base.Equals(obj)) 1143 | return true; 1144 | JSONNumber s2 = obj as JSONNumber; 1145 | if (s2 != null) 1146 | return m_Data == s2.m_Data; 1147 | if (IsNumeric(obj)) 1148 | return Convert.ToDouble(obj) == m_Data; 1149 | return false; 1150 | } 1151 | public override int GetHashCode() 1152 | { 1153 | return m_Data.GetHashCode(); 1154 | } 1155 | public override void Clear() 1156 | { 1157 | m_Data = 0; 1158 | } 1159 | } 1160 | // End of JSONNumber 1161 | 1162 | public partial class JSONBool : JSONNode 1163 | { 1164 | private bool m_Data; 1165 | 1166 | public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } } 1167 | public override bool IsBoolean { get { return true; } } 1168 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1169 | 1170 | public override string Value 1171 | { 1172 | get { return m_Data.ToString(); } 1173 | set 1174 | { 1175 | bool v; 1176 | if (bool.TryParse(value, out v)) 1177 | m_Data = v; 1178 | } 1179 | } 1180 | public override bool AsBool 1181 | { 1182 | get { return m_Data; } 1183 | set { m_Data = value; } 1184 | } 1185 | 1186 | public JSONBool(bool aData) 1187 | { 1188 | m_Data = aData; 1189 | } 1190 | 1191 | public JSONBool(string aData) 1192 | { 1193 | Value = aData; 1194 | } 1195 | 1196 | public override JSONNode Clone() 1197 | { 1198 | return new JSONBool(m_Data); 1199 | } 1200 | 1201 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1202 | { 1203 | aSB.Append((m_Data) ? "true" : "false"); 1204 | } 1205 | public override bool Equals(object obj) 1206 | { 1207 | if (obj == null) 1208 | return false; 1209 | if (obj is bool) 1210 | return m_Data == (bool)obj; 1211 | return false; 1212 | } 1213 | public override int GetHashCode() 1214 | { 1215 | return m_Data.GetHashCode(); 1216 | } 1217 | public override void Clear() 1218 | { 1219 | m_Data = false; 1220 | } 1221 | } 1222 | // End of JSONBool 1223 | 1224 | public partial class JSONNull : JSONNode 1225 | { 1226 | static JSONNull m_StaticInstance = new JSONNull(); 1227 | public static bool reuseSameInstance = true; 1228 | public static JSONNull CreateOrGet() 1229 | { 1230 | if (reuseSameInstance) 1231 | return m_StaticInstance; 1232 | return new JSONNull(); 1233 | } 1234 | private JSONNull() { } 1235 | 1236 | public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } } 1237 | public override bool IsNull { get { return true; } } 1238 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1239 | 1240 | public override string Value 1241 | { 1242 | get { return "null"; } 1243 | set { } 1244 | } 1245 | public override bool AsBool 1246 | { 1247 | get { return false; } 1248 | set { } 1249 | } 1250 | 1251 | public override JSONNode Clone() 1252 | { 1253 | return CreateOrGet(); 1254 | } 1255 | 1256 | public override bool Equals(object obj) 1257 | { 1258 | if (object.ReferenceEquals(this, obj)) 1259 | return true; 1260 | return (obj is JSONNull); 1261 | } 1262 | public override int GetHashCode() 1263 | { 1264 | return 0; 1265 | } 1266 | 1267 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1268 | { 1269 | aSB.Append("null"); 1270 | } 1271 | } 1272 | // End of JSONNull 1273 | 1274 | internal partial class JSONLazyCreator : JSONNode 1275 | { 1276 | private JSONNode m_Node = null; 1277 | private string m_Key = null; 1278 | public override JSONNodeType Tag { get { return JSONNodeType.None; } } 1279 | public override Enumerator GetEnumerator() { return new Enumerator(); } 1280 | 1281 | public JSONLazyCreator(JSONNode aNode) 1282 | { 1283 | m_Node = aNode; 1284 | m_Key = null; 1285 | } 1286 | 1287 | public JSONLazyCreator(JSONNode aNode, string aKey) 1288 | { 1289 | m_Node = aNode; 1290 | m_Key = aKey; 1291 | } 1292 | 1293 | private T Set(T aVal) where T : JSONNode 1294 | { 1295 | if (m_Key == null) 1296 | m_Node.Add(aVal); 1297 | else 1298 | m_Node.Add(m_Key, aVal); 1299 | m_Node = null; // Be GC friendly. 1300 | return aVal; 1301 | } 1302 | 1303 | public override JSONNode this[int aIndex] 1304 | { 1305 | get { return new JSONLazyCreator(this); } 1306 | set { Set(new JSONArray()).Add(value); } 1307 | } 1308 | 1309 | public override JSONNode this[string aKey] 1310 | { 1311 | get { return new JSONLazyCreator(this, aKey); } 1312 | set { Set(new JSONObject()).Add(aKey, value); } 1313 | } 1314 | 1315 | public override void Add(JSONNode aItem) 1316 | { 1317 | Set(new JSONArray()).Add(aItem); 1318 | } 1319 | 1320 | public override void Add(string aKey, JSONNode aItem) 1321 | { 1322 | Set(new JSONObject()).Add(aKey, aItem); 1323 | } 1324 | 1325 | public static bool operator ==(JSONLazyCreator a, object b) 1326 | { 1327 | if (b == null) 1328 | return true; 1329 | return System.Object.ReferenceEquals(a, b); 1330 | } 1331 | 1332 | public static bool operator !=(JSONLazyCreator a, object b) 1333 | { 1334 | return !(a == b); 1335 | } 1336 | 1337 | public override bool Equals(object obj) 1338 | { 1339 | if (obj == null) 1340 | return true; 1341 | return System.Object.ReferenceEquals(this, obj); 1342 | } 1343 | 1344 | public override int GetHashCode() 1345 | { 1346 | return 0; 1347 | } 1348 | 1349 | public override int AsInt 1350 | { 1351 | get { Set(new JSONNumber(0)); return 0; } 1352 | set { Set(new JSONNumber(value)); } 1353 | } 1354 | 1355 | public override float AsFloat 1356 | { 1357 | get { Set(new JSONNumber(0.0f)); return 0.0f; } 1358 | set { Set(new JSONNumber(value)); } 1359 | } 1360 | 1361 | public override double AsDouble 1362 | { 1363 | get { Set(new JSONNumber(0.0)); return 0.0; } 1364 | set { Set(new JSONNumber(value)); } 1365 | } 1366 | 1367 | public override long AsLong 1368 | { 1369 | get 1370 | { 1371 | if (longAsString) 1372 | Set(new JSONString("0")); 1373 | else 1374 | Set(new JSONNumber(0.0)); 1375 | return 0L; 1376 | } 1377 | set 1378 | { 1379 | if (longAsString) 1380 | Set(new JSONString(value.ToString(CultureInfo.InvariantCulture))); 1381 | else 1382 | Set(new JSONNumber(value)); 1383 | } 1384 | } 1385 | 1386 | public override ulong AsULong 1387 | { 1388 | get 1389 | { 1390 | if (longAsString) 1391 | Set(new JSONString("0")); 1392 | else 1393 | Set(new JSONNumber(0.0)); 1394 | return 0L; 1395 | } 1396 | set 1397 | { 1398 | if (longAsString) 1399 | Set(new JSONString(value.ToString(CultureInfo.InvariantCulture))); 1400 | else 1401 | Set(new JSONNumber(value)); 1402 | } 1403 | } 1404 | 1405 | public override bool AsBool 1406 | { 1407 | get { Set(new JSONBool(false)); return false; } 1408 | set { Set(new JSONBool(value)); } 1409 | } 1410 | 1411 | public override JSONArray AsArray 1412 | { 1413 | get { return Set(new JSONArray()); } 1414 | } 1415 | 1416 | public override JSONObject AsObject 1417 | { 1418 | get { return Set(new JSONObject()); } 1419 | } 1420 | internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode) 1421 | { 1422 | aSB.Append("null"); 1423 | } 1424 | } 1425 | // End of JSONLazyCreator 1426 | 1427 | public static class JSON 1428 | { 1429 | public static JSONNode Parse(string aJSON) 1430 | { 1431 | return JSONNode.Parse(aJSON); 1432 | } 1433 | } 1434 | } 1435 | --------------------------------------------------------------------------------