├── .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 |
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 | [](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 | 
139 |
--------------------------------------------------------------------------------
/Installer/Assets/YOUR_PACKAGE_TITLE/README.md:
--------------------------------------------------------------------------------
1 | # Unity Package Template
2 |
3 |
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 | [](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 | 
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 |
--------------------------------------------------------------------------------