diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..8d7bf00
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,5 @@
+# These owners will be the default owners for everything in
+# the repo. Unless a later match takes precedence,
+# @global-owner1 and @global-owner2 will be requested for
+# review when someone opens a pull request.
+* @FejZa @SimonDarksideJ @Cangi
\ No newline at end of file
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..27c9bd7
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [SimonDarksideJ,FejZa]
\ No newline at end of file
diff --git a/.github/workflows/development-buildandtestupmrelease.yml b/.github/workflows/development-buildandtestupmrelease.yml
index 0a37c9a..6e3f40b 100644
--- a/.github/workflows/development-buildandtestupmrelease.yml
+++ b/.github/workflows/development-buildandtestupmrelease.yml
@@ -17,7 +17,7 @@ jobs:
# Check Unity version required by the package
validate-environment:
name: Get Unity Version from UPM package
- uses: realitycollective/reusableworkflows/.github/workflows/getunityversionfrompackage.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/getunityversionfrompackage.yml@v2
with:
build-host: ubuntu-latest
@@ -25,7 +25,7 @@ jobs:
Validate-Unity:
name: Validate Unity Install
needs: validate-environment
- uses: realitycollective/reusableworkflows/.github/workflows/validateunityinstall.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/validateunityinstall.yml@v2
with:
build-target: windows
unityversion: ${{ needs.validate-environment.outputs.unityversion }}
@@ -34,7 +34,7 @@ jobs:
Run-Unit-Tests:
name: Run Unity Unit Tests
needs: Validate-Unity
- uses: realitycollective/reusableworkflows/.github/workflows/rununityUPMbuild.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/rununityUPMbuild.yml@v2
with:
unityversion: ${{ needs.Validate-Unity.outputs.unityeditorversion }}
dependencies: '[{"development": "github.com/realitycollective/com.realitycollective.buildtools.git"},{"development": "github.com/realitycollective/com.realitycollective.utilities.git"}]'
diff --git a/.github/workflows/development-publish.yml b/.github/workflows/development-publish.yml
index 0f8624c..786fd4e 100644
--- a/.github/workflows/development-publish.yml
+++ b/.github/workflows/development-publish.yml
@@ -19,7 +19,7 @@ jobs:
release_on_merge:
if: github.event.pull_request.merged == true
name: Tag and Publish UPM package
- uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2
with:
build-host: ubuntu-latest
build-type: pre-release
diff --git a/.github/workflows/main-publish.yml b/.github/workflows/main-publish.yml
index bfdb70b..70a6068 100644
--- a/.github/workflows/main-publish.yml
+++ b/.github/workflows/main-publish.yml
@@ -12,7 +12,7 @@ jobs:
validate-environment:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, 'no-ver')
name: Get Version from UPM package
- uses: realitycollective/reusableworkflows/.github/workflows/getpackageversionfrompackage.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/getpackageversionfrompackage.yml@v2
with:
build-host: ubuntu-latest
@@ -20,7 +20,7 @@ jobs:
release-Package-only:
needs: validate-environment
name: Release package only, no upversion
- uses: realitycollective/reusableworkflows/.github/workflows/tagrelease.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/tagrelease.yml@v2
with:
build-host: ubuntu-latest
version: ${{ needs.validate-environment.outputs.packageversion }}
@@ -30,7 +30,7 @@ jobs:
upversion-major-Package:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, 'no-ver') == false && contains(github.event.pull_request.title, 'major-release')
name: Major Version package and release
- uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2
with:
build-host: ubuntu-latest
build-type: major
@@ -40,7 +40,7 @@ jobs:
upversion-minor-Package:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, 'no-ver') == false && contains(github.event.pull_request.title, 'minor-release')
name: Minor Version package and release
- uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2
with:
build-host: ubuntu-latest
build-type: minor
@@ -50,7 +50,7 @@ jobs:
upversion-patch-Package:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, 'no-ver') == false && contains(github.event.pull_request.title, 'minor-release') == false && contains(github.event.pull_request.title, 'major-release') == false
name: Patch Version package and release
- uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2
with:
build-host: ubuntu-latest
build-type: patch-release
@@ -70,7 +70,7 @@ jobs:
if: ${{ always() }}
needs: [release-Complete]
name: Refresh development branch
- uses: realitycollective/reusableworkflows/.github/workflows/refreshbranch.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/refreshbranch.yml@v2
with:
build-host: ubuntu-latest
target-branch: development
@@ -82,7 +82,7 @@ jobs:
if: ${{ always() }}
needs: [refresh-development]
name: UpVersion the development branch for the next release
- uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2-beta
+ uses: realitycollective/reusableworkflows/.github/workflows/upversionandtagrelease.yml@v2
with:
build-host: ubuntu-latest
build-type: patch
diff --git a/Editor/BaseProfileInspector.cs b/Editor/BaseProfileInspector.cs
index 1f47513..88438c4 100644
--- a/Editor/BaseProfileInspector.cs
+++ b/Editor/BaseProfileInspector.cs
@@ -66,7 +66,7 @@ protected void RenderHeader(string infoBoxText = "", Texture2D image = null)
if (image.IsNull())
{
- ServiceFrameworkInspectorUtility.RenderMixedRealityToolkitLogo();
+ ServiceFrameworkInspectorUtility.RenderLogo();
}
else
{
diff --git a/Editor/Packages.meta b/Editor/Packages.meta
new file mode 100644
index 0000000..c35202c
--- /dev/null
+++ b/Editor/Packages.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: faa11f3fd65ae4545916bc9d294b58e4
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Packages/AssetsInstaller.cs b/Editor/Packages/AssetsInstaller.cs
new file mode 100644
index 0000000..ac4a9c0
--- /dev/null
+++ b/Editor/Packages/AssetsInstaller.cs
@@ -0,0 +1,186 @@
+// Copyright (c) Reality Collective. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using RealityCollective.Editor.Utilities;
+using RealityCollective.Extensions;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+namespace RealityCollective.ServiceFramework.Editor.Packages
+{
+ ///
+ /// Installs plugin package assets.
+ ///
+ public static class AssetsInstaller
+ {
+ ///
+ /// Contains information about an
+ /// operation.
+ ///
+ public struct AssetInstallerEventArgs
+ {
+ ///
+ /// List of paths to assets intalled into the project.
+ ///
+ public List InstalledAssets { get; set; }
+
+ ///
+ /// If set, a silent install was requested.
+ ///
+ public bool SkipDialog { get; set; }
+ }
+
+ ///
+ /// The installer has installed package assets to the project.
+ ///
+ public static Action AssetsInstalled;
+
+ ///
+ /// Attempt to copy any assets found in the source path into the project.
+ ///
+ /// The source path of the assets to be installed. This should typically be from a hidden upm package folder marked with a "~".
+ /// The destination path, typically inside the projects "Assets" directory.
+ /// Should the guids for the copied assets be regenerated?
+ /// If set, assets and configuration is installed without prompting the user.
+ /// true if the assets were successfully installed to the project.
+ public static bool TryInstallAssets(string sourcePath, string destinationPath, bool regenerateGuids = false, bool skipDialog = false)
+ => TryInstallAssets(new Dictionary { { sourcePath, destinationPath } }, regenerateGuids, skipDialog);
+
+ ///
+ /// Attempt to copy any assets found in the source path into the project.
+ ///
+ /// The assets paths to be installed. Key is the source path of the assets to be installed. This should typically be from a hidden upm package folder marked with a "~". Value is the destination.
+ /// Should the guids for the copied assets be regenerated?
+ /// If set, assets and configuration is installed without prompting the user.
+ /// true if the assets were successfully installed to the project.
+ public static bool TryInstallAssets(Dictionary installationPaths, bool regenerateGuids = false, bool skipDialog = false)
+ {
+ var anyFail = false;
+ var newInstall = true;
+ var installedAssets = new List();
+ var installedDirectories = new List();
+
+ foreach (var installationPath in installationPaths)
+ {
+ var sourcePath = installationPath.Key.BackSlashes();
+ var destinationPath = installationPath.Value.BackSlashes();
+ installedDirectories.Add(destinationPath);
+
+ if (Directory.Exists(destinationPath))
+ {
+ newInstall = false;
+ EditorUtility.DisplayProgressBar("Verifying assets...", $"{sourcePath} -> {destinationPath}", 0);
+
+ installedAssets.AddRange(UnityFileHelper.GetUnityAssetsAtPath(destinationPath));
+
+ for (int i = 0; i < installedAssets.Count; i++)
+ {
+ EditorUtility.DisplayProgressBar("Verifying assets...", Path.GetFileNameWithoutExtension(installedAssets[i]), i / (float)installedAssets.Count);
+ installedAssets[i] = installedAssets[i].Replace($"{PackageInstaller.ProjectRootPath}{Path.DirectorySeparatorChar}", string.Empty).BackSlashes();
+ }
+
+ EditorUtility.ClearProgressBar();
+ }
+ else
+ {
+ var destinationDirectory = Path.GetFullPath(destinationPath);
+
+ // Check if directory or symbolic link exists before attempting to create it
+ if (!Directory.Exists(destinationDirectory) &&
+ !File.Exists(destinationDirectory))
+ {
+ Directory.CreateDirectory(destinationDirectory);
+ }
+
+ EditorUtility.DisplayProgressBar("Copying assets...", $"{sourcePath} -> {destinationPath}", 0);
+
+ var copiedAssets = UnityFileHelper.GetUnityAssetsAtPath(sourcePath);
+
+ for (var i = 0; i < copiedAssets.Count; i++)
+ {
+ EditorUtility.DisplayProgressBar("Copying assets...", Path.GetFileNameWithoutExtension(copiedAssets[i]), i / (float)copiedAssets.Count);
+
+ try
+ {
+ copiedAssets[i] = CopyAsset(sourcePath, copiedAssets[i], destinationPath);
+ }
+ catch (Exception e)
+ {
+ Debug.LogError(e);
+ anyFail = true;
+ }
+ }
+
+ if (!anyFail)
+ {
+ installedAssets.AddRange(copiedAssets);
+ }
+
+ EditorUtility.ClearProgressBar();
+ }
+ }
+
+ if (anyFail)
+ {
+ foreach (var installedDirectory in installedDirectories)
+ {
+ try
+ {
+ if (Directory.Exists(installedDirectory))
+ {
+ Directory.Delete(installedDirectory);
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.LogError(e);
+ }
+ }
+ }
+
+ if (newInstall && regenerateGuids)
+ {
+ GuidRegenerator.RegenerateGuids(installedDirectories);
+ }
+
+ EditorUtility.ClearProgressBar();
+ AssetsInstalled?.Invoke(new AssetInstallerEventArgs
+ {
+ InstalledAssets = installedAssets,
+ SkipDialog = skipDialog
+ });
+
+ return true;
+ }
+
+ private static string CopyAsset(this string rootPath, string sourceAssetPath, string destinationPath)
+ {
+ sourceAssetPath = sourceAssetPath.BackSlashes();
+ destinationPath = $"{destinationPath}{sourceAssetPath.Replace(Path.GetFullPath(rootPath), string.Empty)}".BackSlashes();
+ destinationPath = Path.Combine(PackageInstaller.ProjectRootPath, destinationPath).BackSlashes();
+
+ if (!File.Exists(destinationPath))
+ {
+ if (!Directory.Exists(Directory.GetParent(destinationPath).FullName))
+ {
+ Directory.CreateDirectory(Directory.GetParent(destinationPath).FullName);
+ }
+
+ try
+ {
+ File.Copy(sourceAssetPath, destinationPath);
+ }
+ catch
+ {
+ Debug.LogError($"$Failed to copy asset!\n{sourceAssetPath}\n{destinationPath}");
+ throw;
+ }
+ }
+
+ return destinationPath.Replace($"{PackageInstaller.ProjectRootPath}{Path.DirectorySeparatorChar}", string.Empty);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/Packages/AssetsInstaller.cs.meta b/Editor/Packages/AssetsInstaller.cs.meta
new file mode 100644
index 0000000..fed7ae0
--- /dev/null
+++ b/Editor/Packages/AssetsInstaller.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e009120ab427f464f90e5e814b29ce7f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Packages/IPackageModulesInstaller.cs b/Editor/Packages/IPackageModulesInstaller.cs
new file mode 100644
index 0000000..35fd484
--- /dev/null
+++ b/Editor/Packages/IPackageModulesInstaller.cs
@@ -0,0 +1,20 @@
+using RealityCollective.ServiceFramework.Definitions;
+using RealityCollective.ServiceFramework.Interfaces;
+
+namespace RealityCollective.ServiceFramework.Editor.Packages
+{
+ ///
+ /// A package installer that will install s coming from a third
+ /// party package into the respective 's .
+ ///
+ public interface IPackageModulesInstaller
+ {
+ ///
+ /// Installs the .
+ ///
+ /// The containing
+ /// the configured to install.
+ /// true, if the was approved and installed.
+ bool Install(ServiceConfiguration serviceConfiguration);
+ }
+}
\ No newline at end of file
diff --git a/Editor/Packages/IPackageModulesInstaller.cs.meta b/Editor/Packages/IPackageModulesInstaller.cs.meta
new file mode 100644
index 0000000..26320ef
--- /dev/null
+++ b/Editor/Packages/IPackageModulesInstaller.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 17c6822e87e61c540806332ec58d9372
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Packages/PackageInstaller.cs b/Editor/Packages/PackageInstaller.cs
new file mode 100644
index 0000000..e6c57e9
--- /dev/null
+++ b/Editor/Packages/PackageInstaller.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Reality Collective. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using RealityCollective.Extensions;
+using RealityCollective.ServiceFramework.Definitions;
+using RealityCollective.ServiceFramework.Interfaces;
+using RealityCollective.ServiceFramework.Services;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+
+namespace RealityCollective.ServiceFramework.Editor.Packages
+{
+ ///
+ /// Installs a service framework plugin package's s and s
+ /// to a .
+ ///
+ public static class PackageInstaller
+ {
+ private static List modulesInstallers = new List();
+
+ public static string ProjectRootPath => Directory.GetParent(Application.dataPath).FullName.BackSlashes();
+
+ ///
+ /// Registers a with the package installer. The
+ /// will be consulted by the packgae installer whenever a needs to be installed into its parent service
+ /// profile.
+ ///
+ ///
+ public static void RegisterModulesInstaller(IPackageModulesInstaller modulesInstaller)
+ => modulesInstallers.EnsureListItem(modulesInstaller);
+
+ ///
+ /// Installs the s contained in the to the provided .
+ ///
+ /// The to install.
+ public static void InstallPackage(PackageInstallerProfile packageInstallerProfile)
+ {
+ if (ServiceManager.Instance == null ||
+ ServiceManager.Instance.ActiveProfile.IsNull())
+ {
+ Debug.LogError($"Cannot install service configurations. There is no active {nameof(ServiceManager)} or it does not have a valid profile.");
+ return;
+ }
+
+ var rootProfile = ServiceManager.Instance.ActiveProfile;
+ var didInstallConfigurations = false;
+ foreach (var configuration in packageInstallerProfile.Configurations)
+ {
+ try
+ {
+ var configurationType = configuration.InstancedType.Type;
+
+ if (configurationType == null)
+ {
+ Debug.LogError($"Failed to find a valid {nameof(configuration.InstancedType)} for {configuration.Name}!");
+ continue;
+ }
+
+ // If the service to install is a service module, we have to lookup the service module installer
+ // for that specific module type and ask it to install the module.
+ if (typeof(IServiceModule).IsAssignableFrom(configurationType))
+ {
+ // Check with all registered module installers, whether the module is a fit.
+ var didInstallServiceModule = false;
+ foreach (var modulesInstaller in modulesInstallers)
+ {
+ didInstallServiceModule = modulesInstaller.Install(configuration);
+ if (didInstallServiceModule)
+ {
+ break;
+ }
+ }
+
+ if (!didInstallServiceModule)
+ {
+ Debug.LogError($"Unable to install {configurationType.Name}. Installation was denied by the installer or no module installer was available for type {configurationType.Name}.");
+ }
+ }
+ // If the service is a top level service, we only need to make sure that the service is not already installed.
+ // in the target profile.
+ else if (typeof(IService).IsAssignableFrom(configurationType))
+ {
+ // Setup the configuration.
+ var serviceConfiguration = new ServiceConfiguration(configurationType, configuration.Name, configuration.Priority, configuration.RuntimePlatforms, configuration.Profile);
+
+ // Make sure it's not already in the target profile.
+ if (rootProfile.ServiceConfigurations.All(sc => sc.InstancedType.Type != serviceConfiguration.InstancedType.Type))
+ {
+ // Bada bing bada boom, install the service to the target profile.
+ rootProfile.AddConfiguration(serviceConfiguration);
+ EditorUtility.SetDirty(rootProfile);
+ didInstallConfigurations = true;
+ Debug.Log($"Installed {serviceConfiguration.Name}.");
+ }
+ else
+ {
+ Debug.Log($"Skipped installing {serviceConfiguration.Name}. Already installed.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ Debug.LogError($"Failed to install {configuration.Name}.");
+ }
+ }
+
+ AssetDatabase.SaveAssets();
+ EditorApplication.delayCall += () => AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
+
+ if (didInstallConfigurations)
+ {
+ ServiceManager.Instance.ResetProfile(ServiceManager.Instance.ActiveProfile);
+ }
+ }
+ }
+}
diff --git a/Editor/Packages/PackageInstaller.cs.meta b/Editor/Packages/PackageInstaller.cs.meta
new file mode 100644
index 0000000..8ff9cd7
--- /dev/null
+++ b/Editor/Packages/PackageInstaller.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: de1f4e3094f569d43b654e2dfadb1028
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Packages/PackageInstallerProfile.cs b/Editor/Packages/PackageInstallerProfile.cs
new file mode 100644
index 0000000..e97061e
--- /dev/null
+++ b/Editor/Packages/PackageInstallerProfile.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Reality Collective. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using RealityCollective.ServiceFramework.Definitions;
+using UnityEngine;
+
+namespace RealityCollective.ServiceFramework.Editor.Packages
+{
+ ///
+ /// A package profile defines services and modules that the package has to offer and may be installed
+ /// to a to register those services and modules with the service container.
+ ///
+ [CreateAssetMenu(menuName = "Reality Collective/Service Framework/" + nameof(PackageInstallerProfile), fileName = nameof(PackageInstallerProfile), order = (int)CreateProfileMenuItemIndices.Configuration)]
+ public class PackageInstallerProfile : BaseProfile
+ {
+ [SerializeField, Tooltip("The platforms the package can run on.")]
+ private RuntimePlatformEntry platformEntries = new RuntimePlatformEntry();
+
+ ///
+ /// The platforms the package can run on.
+ ///
+ public RuntimePlatformEntry PlatformEntries => platformEntries;
+
+ [SerializeField, Tooltip("The service and module configurations of the package.")]
+ private ServiceConfiguration[] configurations = new ServiceConfiguration[0];
+
+ ///
+ /// The service and module configurations of the package that may be installed.
+ ///
+ public ServiceConfiguration[] Configurations => configurations;
+ }
+}
\ No newline at end of file
diff --git a/Editor/Packages/PackageInstallerProfile.cs.meta b/Editor/Packages/PackageInstallerProfile.cs.meta
new file mode 100644
index 0000000..e8f9f80
--- /dev/null
+++ b/Editor/Packages/PackageInstallerProfile.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: da64fb88269149f8a95f1fb969d6a0b4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Packages/PackageInstallerProfileInspector.cs b/Editor/Packages/PackageInstallerProfileInspector.cs
new file mode 100644
index 0000000..f1b703f
--- /dev/null
+++ b/Editor/Packages/PackageInstallerProfileInspector.cs
@@ -0,0 +1,361 @@
+// Copyright (c) Reality Collective. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using RealityCollective.Definitions.Utilities;
+using RealityCollective.Editor.Extensions;
+using RealityCollective.Extensions;
+using RealityCollective.ServiceFramework.Attributes;
+using RealityCollective.ServiceFramework.Definitions;
+using RealityCollective.ServiceFramework.Editor.Profiles;
+using RealityCollective.ServiceFramework.Editor.PropertyDrawers;
+using RealityCollective.ServiceFramework.Services;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEditorInternal;
+using UnityEngine;
+
+namespace RealityCollective.ServiceFramework.Editor.Packages
+{
+ [CustomEditor(typeof(PackageInstallerProfile))]
+ public class PackageInstallerProfileInspector : BaseProfileInspector
+ {
+ private readonly GUIContent profileContent = new GUIContent("Profile", "The settings profile for this package.");
+ private ReorderableList configurationList;
+ private int currentlySelectedConfigurationOption;
+
+ private SerializedProperty configurations;
+ private SerializedProperty platformEntries;
+
+ private Type[] platforms = new Type[0];
+ private List> configListHeightFlags;
+
+ protected override void OnEnable()
+ {
+ base.OnEnable();
+
+ configurations = serializedObject.FindProperty(nameof(configurations));
+ platformEntries = serializedObject.FindProperty(nameof(platformEntries));
+ UpdatePlatformList();
+ Debug.Assert(configurations != null);
+
+ configurationList = new ReorderableList(serializedObject, configurations, true, false, true, true);
+ configListHeightFlags = new List>(configurations.arraySize);
+
+ for (int i = 0; i < configurations.arraySize; i++)
+ {
+ configListHeightFlags.Add(new Tuple(true, false));
+ }
+
+ configurationList.drawElementCallback += DrawConfigurationOptionElement;
+ configurationList.onAddCallback += OnConfigurationOptionAdded;
+ configurationList.onRemoveCallback += OnConfigurationOptionRemoved;
+ configurationList.elementHeightCallback += ElementHeightCallback;
+ }
+
+ private void UpdatePlatformList()
+ {
+ SerializedProperty runtimePlatforms;
+ runtimePlatforms = platformEntries.FindPropertyRelative(nameof(runtimePlatforms));
+
+ if (runtimePlatforms.arraySize > 0)
+ {
+ if (platforms.Length != runtimePlatforms.arraySize)
+ {
+ platforms = new Type[runtimePlatforms.arraySize];
+ }
+
+ for (int i = 0; i < runtimePlatforms.arraySize; i++)
+ {
+ platforms[i] = new SystemType(runtimePlatforms.GetArrayElementAtIndex(i));
+ }
+ }
+ else
+ {
+ platforms = new Type[0];
+ }
+ }
+
+ public override void OnInspectorGUI()
+ {
+ RenderHeader("Use this configuration profile to setup all of the services you would like to add to any existing profile configurations when the target package is installed.");
+ EditorGUILayout.Space();
+
+ if (GUILayout.Button("Install Package Service Configuration"))
+ {
+ if (ServiceManager.IsActiveAndInitialized && ServiceManager.Instance.HasActiveProfile)
+ {
+ EditorApplication.delayCall += () => PackageInstaller.InstallPackage(target as PackageInstallerProfile);
+ }
+ else
+ {
+ EditorUtility.DisplayDialog("Attention!", "Unable to install profile as the Service Framework could not be found.\nIs there a Service Framework instance in the scene?", "OK");
+ return;
+ }
+ }
+
+ EditorGUILayout.Space();
+ EditorGUILayout.Space();
+
+ serializedObject.Update();
+
+ EditorGUI.BeginChangeCheck();
+ EditorGUILayout.PropertyField(platformEntries);
+
+ if (EditorGUI.EndChangeCheck())
+ {
+ UpdatePlatformList();
+ }
+
+ EditorGUILayout.Space();
+ configurations.isExpanded = EditorGUILayoutExtensions.FoldoutWithBoldLabel(configurations.isExpanded, new GUIContent("Configuration Options"));
+
+ if (configurations.isExpanded)
+ {
+ EditorGUILayout.Space();
+ configurationList.DoLayoutList();
+
+ if (configurations == null || configurations.arraySize == 0)
+ {
+ EditorGUILayout.HelpBox("Register a new Service Configuration", MessageType.Warning);
+ }
+
+ }
+
+ serializedObject.ApplyModifiedProperties();
+ }
+
+ private float ElementHeightCallback(int index)
+ {
+ if (configListHeightFlags.Count == 0)
+ {
+ return EditorGUIUtility.singleLineHeight;
+ }
+
+ var (isExpanded, hasProfile) = configListHeightFlags[index];
+ var modifier = isExpanded
+ ? hasProfile
+ ? 5.5f
+ : 4f
+ : 1.5f;
+ return EditorGUIUtility.singleLineHeight * modifier;
+ }
+
+ private void DrawConfigurationOptionElement(Rect rect, int index, bool isActive, bool isFocused)
+ {
+ if (isFocused)
+ {
+ currentlySelectedConfigurationOption = index;
+ }
+
+ serializedObject.Update();
+
+ var configurationProperty = configurations.GetArrayElementAtIndex(index);
+
+ SerializedProperty instancedType;
+ SerializedProperty priority;
+ SerializedProperty runtimePlatforms;
+ SerializedProperty profile;
+
+ var nameProperty = configurationProperty.FindPropertyRelative(nameof(name));
+ priority = configurationProperty.FindPropertyRelative(nameof(priority));
+ instancedType = configurationProperty.FindPropertyRelative(nameof(instancedType));
+ var platformEntriesProperty = configurationProperty.FindPropertyRelative(nameof(platformEntries));
+ var globalRuntimePlatforms = platformEntries.FindPropertyRelative(nameof(runtimePlatforms));
+ runtimePlatforms = platformEntriesProperty.FindPropertyRelative(nameof(runtimePlatforms));
+ profile = configurationProperty.FindPropertyRelative(nameof(profile));
+ var systemTypeReference = new SystemType(instancedType);
+
+ bool addPlatforms = false;
+
+ if (runtimePlatforms.arraySize != globalRuntimePlatforms.arraySize)
+ {
+ addPlatforms = true;
+ runtimePlatforms.ClearArray();
+ }
+
+ if (globalRuntimePlatforms.arraySize > 0)
+ {
+ for (int i = 0; i < globalRuntimePlatforms.arraySize; i++)
+ {
+ if (addPlatforms)
+ {
+ runtimePlatforms.InsertArrayElementAtIndex(i);
+ }
+
+ SerializedProperty reference;
+ reference = globalRuntimePlatforms.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(reference));
+ runtimePlatforms.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(reference)).stringValue = reference.stringValue;
+ }
+ }
+
+ var hasProfile = false;
+
+ Type profileType = null;
+
+ if (systemTypeReference.Type != null)
+ {
+ if (nameProperty.stringValue.Contains("New Configuration"))
+ {
+ nameProperty.stringValue = systemTypeReference.Type.Name.ToProperCase();
+ }
+
+ var constructors = systemTypeReference.Type.GetConstructors();
+
+ foreach (var constructorInfo in constructors)
+ {
+ var parameters = constructorInfo.GetParameters();
+
+ foreach (var parameterInfo in parameters)
+ {
+ if (parameterInfo.ParameterType.IsAbstract) { continue; }
+
+ if (parameterInfo.ParameterType.IsSubclassOf(typeof(BaseProfile)))
+ {
+ profileType = parameterInfo.ParameterType;
+ break;
+ }
+ }
+
+ if (profileType != null)
+ {
+ hasProfile = true;
+ break;
+ }
+ }
+ }
+
+ priority.intValue = index;
+
+ var lastMode = EditorGUIUtility.wideMode;
+ var prevLabelWidth = EditorGUIUtility.labelWidth;
+
+ EditorGUIUtility.labelWidth = prevLabelWidth - 18f;
+ EditorGUIUtility.wideMode = true;
+
+ var halfFieldHeight = EditorGUIUtility.singleLineHeight * 0.25f;
+
+ var rectX = rect.x + 12;
+ var rectWidth = rect.width - 12;
+ var nameRect = new Rect(rectX, rect.y + halfFieldHeight, rectWidth, EditorGUIUtility.singleLineHeight);
+ var typeRect = new Rect(rectX, rect.y + halfFieldHeight * 6, rectWidth, EditorGUIUtility.singleLineHeight);
+ var profileRect = new Rect(rectX, rect.y + halfFieldHeight * 11, rectWidth, EditorGUIUtility.singleLineHeight);
+ var runtimeRect = new Rect(rectX, rect.y + halfFieldHeight * (hasProfile ? 16 : 11), rectWidth, EditorGUIUtility.singleLineHeight);
+
+ if (configurationProperty.isExpanded)
+ {
+ EditorGUI.PropertyField(nameRect, nameProperty);
+ configurationProperty.isExpanded = EditorGUI.Foldout(nameRect, configurationProperty.isExpanded, GUIContent.none, true);
+
+ if (!configurationProperty.isExpanded)
+ {
+ GUI.FocusControl(null);
+ }
+ }
+ else
+ {
+ configurationProperty.isExpanded = EditorGUI.Foldout(nameRect, configurationProperty.isExpanded, nameProperty.stringValue, true);
+ }
+
+ var hasChanged = false;
+
+ if (configurationProperty.isExpanded)
+ {
+ EditorGUI.BeginChangeCheck();
+ TypeReferencePropertyDrawer.FilterConstraintOverride = IsConstraintSatisfied;
+ TypeReferencePropertyDrawer.GroupingOverride = TypeGrouping.NoneByNameNoNamespace;
+ EditorGUI.PropertyField(typeRect, instancedType);
+ systemTypeReference = new SystemType(instancedType);
+
+ GUI.enabled = false;
+
+ EditorGUI.PropertyField(runtimeRect, platformEntriesProperty);
+ GUI.enabled = true;
+
+ if (hasProfile)
+ {
+ ProfilePropertyDrawer.ProfileTypeOverride = profileType;
+ EditorGUI.PropertyField(profileRect, profile, profileContent);
+ }
+
+ hasChanged = EditorGUI.EndChangeCheck() &&
+ runtimePlatforms.arraySize > 0 &&
+ systemTypeReference.Type != null;
+ }
+
+ serializedObject.ApplyModifiedProperties();
+
+ if (profile.objectReferenceValue != null)
+ {
+ var renderedProfile = profile.objectReferenceValue as BaseProfile;
+ Debug.Assert(renderedProfile != null);
+
+ if (renderedProfile.ParentProfile.IsNull() ||
+ renderedProfile.ParentProfile != ThisProfile)
+ {
+ renderedProfile.ParentProfile = ThisProfile;
+ }
+ }
+
+ if (ServiceManager.IsActiveAndInitialized && hasChanged)
+ {
+ ServiceManager.Instance.ResetProfile(ServiceManager.Instance.ActiveProfile);
+ }
+
+ EditorGUIUtility.wideMode = lastMode;
+ EditorGUIUtility.labelWidth = prevLabelWidth;
+ configListHeightFlags[index] = new Tuple(configurationProperty.isExpanded, hasProfile);
+ }
+
+ private bool IsConstraintSatisfied(Type type)
+ {
+ var platformActive = false;
+
+ foreach (var attribute in Attribute.GetCustomAttributes(type, typeof(RuntimePlatformAttribute)))
+ {
+ if (attribute is RuntimePlatformAttribute platformAttribute &&
+ platforms.Contains(platformAttribute.Platform))
+ {
+ platformActive = true;
+ }
+ }
+
+ return !type.IsAbstract && platformActive;
+ }
+
+ private void OnConfigurationOptionAdded(ReorderableList list)
+ {
+ configurations.arraySize += 1;
+ var index = configurations.arraySize - 1;
+
+ var configuration = new ConfigurationProperty(configurations.GetArrayElementAtIndex(index), true)
+ {
+ IsExpanded = true,
+ Name = $"New Configuration {index}",
+ InstancedType = null,
+ Priority = (uint)index,
+ Profile = null,
+ };
+
+ configuration.ApplyModifiedProperties();
+ configListHeightFlags.Add(new Tuple(true, false));
+ serializedObject.ApplyModifiedProperties();
+ }
+
+ private void OnConfigurationOptionRemoved(ReorderableList list)
+ {
+ if (currentlySelectedConfigurationOption >= 0)
+ {
+ configurations.DeleteArrayElementAtIndex(currentlySelectedConfigurationOption);
+ }
+
+ serializedObject.ApplyModifiedProperties();
+
+ if (ServiceManager.IsActiveAndInitialized)
+ {
+ EditorApplication.delayCall += () => ServiceManager.Instance.ResetProfile(ServiceManager.Instance.ActiveProfile);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/Packages/PackageInstallerProfileInspector.cs.meta b/Editor/Packages/PackageInstallerProfileInspector.cs.meta
new file mode 100644
index 0000000..7018f3a
--- /dev/null
+++ b/Editor/Packages/PackageInstallerProfileInspector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c25b0b6c70074dcebd0d3afc9be44615
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Packages/PackageInstallerWizard.cs b/Editor/Packages/PackageInstallerWizard.cs
new file mode 100644
index 0000000..3d7cd61
--- /dev/null
+++ b/Editor/Packages/PackageInstallerWizard.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Reality Collective. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using RealityCollective.Editor.Extensions;
+using RealityCollective.Extensions;
+using RealityCollective.ServiceFramework.Definitions;
+using RealityCollective.ServiceFramework.Services;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+
+namespace RealityCollective.ServiceFramework.Editor.Packages
+{
+ [InitializeOnLoad]
+ public static class PackageInstallerWizard
+ {
+ static PackageInstallerWizard()
+ {
+ AssetsInstaller.AssetsInstalled += AssetsInstaller_AssetsInstalled;
+ }
+
+ private static void AssetsInstaller_AssetsInstalled(AssetsInstaller.AssetInstallerEventArgs eventArgs)
+ {
+ if (!Application.isBatchMode)
+ {
+ EditorApplication.delayCall += () =>
+ {
+ AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
+ EditorApplication.delayCall += () =>
+ AddConfigurations(eventArgs.InstalledAssets, eventArgs.SkipDialog);
+ };
+ }
+ }
+
+ private static void AddConfigurations(List profiles, bool skipDialog = false)
+ {
+ if (skipDialog)
+ {
+ return;
+ }
+
+ ServiceProvidersProfile rootProfile;
+
+ if (ServiceManager.IsActiveAndInitialized)
+ {
+ rootProfile = ServiceManager.Instance.ActiveProfile;
+ }
+ else
+ {
+ var availableRootProfiles = ScriptableObjectExtensions.GetAllInstances();
+ rootProfile = availableRootProfiles.Length > 0 ? availableRootProfiles[0] : null;
+ }
+
+ // Only if a root profile is available at all it makes sense to display the
+ // packkage configuration import dialog. If the user does not have a root profile yet,
+ // for whatever reason, there is nothing we can do here.
+ if (rootProfile.IsNull())
+ {
+ EditorUtility.DisplayDialog("Attention!", $"Each service and service module in the package will need to be manually registered as no existing Service Framework Instance was found.\nUse the {nameof(PackageInstallerProfile)} in the Profiles folder for the package once a Service Manager has been configured.", "OK");
+ return;
+ }
+
+ Selection.activeObject = null;
+
+ foreach (var profile in profiles.Where(x => x.EndsWith(".asset")))
+ {
+ var packageInstallerProfile = AssetDatabase.LoadAssetAtPath(profile);
+ if (packageInstallerProfile.IsNull())
+ {
+ continue;
+ }
+
+ if (EditorUtility.DisplayDialog("New package detected",
+ $"We found the {packageInstallerProfile.name.ToProperCase()}. Would you like to add this package configuration to your {rootProfile.name}?",
+ "Yes!",
+ "later"))
+ {
+ PackageInstaller.InstallPackage(packageInstallerProfile);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/Packages/PackageInstallerWizard.cs.meta b/Editor/Packages/PackageInstallerWizard.cs.meta
new file mode 100644
index 0000000..51a47e5
--- /dev/null
+++ b/Editor/Packages/PackageInstallerWizard.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 274b6ae4a7cac0243bf75f748f96ad81
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/PathFinderUtility.cs b/Editor/PathFinderUtility.cs
new file mode 100644
index 0000000..4c29e53
--- /dev/null
+++ b/Editor/PathFinderUtility.cs
@@ -0,0 +1,105 @@
+// Copyright (c) RealityCollective. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using RealityCollective.Extensions;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+
+namespace RealityCollective.ServiceFramework.Editor
+{
+ ///
+ /// Interface to implement on a to make it easier to find relative/absolute folder paths using the .
+ ///
+ ///
+ /// Required to be a standalone class in a separate file or else returns an empty string path.
+ ///
+ public interface IPathFinder
+ {
+ ///
+ /// The relative path to this class from either the Assets or Packages folder.
+ ///
+ string Location { get; }
+ }
+
+ public static class PathFinderUtility
+ {
+ private static readonly Dictionary PathFinderCache = new Dictionary();
+ private static readonly Dictionary ResolvedFinderCache = new Dictionary();
+
+ private static List GetAllPathFinders
+ {
+ get
+ {
+ return AppDomain.CurrentDomain.GetAssemblies()
+ .SelectMany(assembly => assembly.GetTypes())
+ .Where(type => typeof(IPathFinder).IsAssignableFrom(type) && type.IsClass && !type.IsAbstract)
+ .OrderBy(type => type.Name)
+ .ToList();
+ }
+ }
+
+ public static string ResolvePath(string finderPath)
+ {
+ if (!ResolvedFinderCache.TryGetValue(finderPath, out var resolvedPath))
+ {
+ foreach (var type in GetAllPathFinders)
+ {
+ if (type.Name == Path.GetFileNameWithoutExtension(finderPath))
+ {
+ resolvedPath = AssetDatabase.GetAssetPath(
+ MonoScript.FromScriptableObject(
+ ScriptableObject.CreateInstance(type)))
+ .Replace(finderPath, string.Empty);
+ ResolvedFinderCache.Add(finderPath, resolvedPath);
+ break;
+ }
+ }
+ }
+
+ return resolvedPath;
+ }
+
+ ///
+ /// Resolves the path to the provided .
+ ///
+ /// constraint.
+ /// The of to resolve the path for.
+ /// If found, the relative path to the root folder this references.
+ public static string ResolvePath(Type pathFinderType) where T : IPathFinder
+ {
+ if (pathFinderType is null)
+ {
+ Debug.LogError($"{nameof(pathFinderType)} is null!");
+ return null;
+ }
+
+ if (!typeof(T).IsAssignableFrom(pathFinderType))
+ {
+ Debug.LogError($"{pathFinderType.Name} must implement {nameof(IPathFinder)}");
+ return null;
+ }
+
+ if (!typeof(ScriptableObject).IsAssignableFrom(pathFinderType))
+ {
+ Debug.LogError($"{pathFinderType.Name} must derive from {nameof(ScriptableObject)}");
+ return null;
+ }
+
+ if (!PathFinderCache.TryGetValue(pathFinderType, out var resolvedPath))
+ {
+ var pathFinder = ScriptableObject.CreateInstance(pathFinderType) as IPathFinder;
+ Debug.Assert(pathFinder != null, $"{nameof(pathFinder)} != null");
+ resolvedPath = AssetDatabase.GetAssetPath(
+ MonoScript.FromScriptableObject((ScriptableObject)pathFinder))
+ .Replace(pathFinder.Location, string.Empty);
+ PathFinderCache.Add(pathFinderType, resolvedPath);
+ }
+
+ return resolvedPath.BackSlashes();
+ }
+ }
+}
diff --git a/Editor/PathFinderUtility.cs.meta b/Editor/PathFinderUtility.cs.meta
new file mode 100644
index 0000000..ac13006
--- /dev/null
+++ b/Editor/PathFinderUtility.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: da8b529a856046cdb4f5da06a89f0305
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/ServiceFrameworkPreferences.cs b/Editor/ServiceFrameworkPreferences.cs
index 0c56852..2933391 100644
--- a/Editor/ServiceFrameworkPreferences.cs
+++ b/Editor/ServiceFrameworkPreferences.cs
@@ -20,7 +20,7 @@ public static class ServiceFrameworkPreferences
public const string Service_Framework_Editor_Menu_Keyword = Editor_Menu_Keyword + "/Service Framework";
- private static readonly string[] Package_Keywords = { "RealityCollective", "Mixed", "Reality", "ServiceFramework" };
+ private static readonly string[] Package_Keywords = { "Reality", "Collective", "Mixed", "Reality", "Service", "Framework" };
public static readonly HashSet ExcludedTemplateServices = new HashSet
{
@@ -60,7 +60,7 @@ public static bool ShowInspectorDebugView
private static IPlatform currentPlatformTarget = null;
///
- /// The current target.
+ /// The current target.
///
public static IPlatform CurrentPlatformTarget
{
diff --git a/Editor/ServiceManagerInspector.cs b/Editor/ServiceManagerInspector.cs
index f095cab..e71d6ac 100644
--- a/Editor/ServiceManagerInspector.cs
+++ b/Editor/ServiceManagerInspector.cs
@@ -51,7 +51,7 @@ private void OnDestroy()
public override void OnInspectorGUI()
{
- ServiceFrameworkInspectorUtility.RenderMixedRealityToolkitLogo();
+ ServiceFrameworkInspectorUtility.RenderLogo();
serializedObject.Update();
EditorGUI.BeginChangeCheck();
diff --git a/Editor/Utilities/ServiceFrameworkInspectorUtility.cs b/Editor/Utilities/ServiceFrameworkInspectorUtility.cs
index 53e7175..bf1ebd9 100644
--- a/Editor/Utilities/ServiceFrameworkInspectorUtility.cs
+++ b/Editor/Utilities/ServiceFrameworkInspectorUtility.cs
@@ -174,7 +174,7 @@ public static Texture2D LightThemeLogo
///
/// Render the Mixed Reality Toolkit Logo.
///
- public static void RenderMixedRealityToolkitLogo()
+ public static void RenderLogo()
{
RenderInspectorHeader(EditorGUIUtility.isProSkin ? DarkThemeLogo : LightThemeLogo);
}
diff --git a/README.md b/README.md
index 1c53ce5..c2e8502 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,11 @@
# Service Framework
-The Service Framework package components for the [Reality Collective](https://realityCollective.io). This package an extensible service framework to build highly performant components for your Unity projects.
+The Service Framework package by the [Reality Collective](https://www.realityCollective.io). This package is an extensible service framework to build highly performant components for your Unity projects.
[![openupm](https://img.shields.io/npm/v/com.realitycollective.service-framework?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.realitycollective.service-framework/)
+[![Discord](https://img.shields.io/discord/597064584980987924.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/hF7TtRCFmB)
+[![Publish development branch on Merge](https://github.com/realitycollective/com.realitycollective.service-framework/actions/workflows/development-publish.yml/badge.svg)](https://github.com/realitycollective/com.realitycollective.service-framework/actions/workflows/development-publish.yml)
+[![Build and test UPM packages for platforms, all branches except main](https://github.com/realitycollective/com.realitycollective.service-framework/actions/workflows/development-buildandtestupmrelease.yml/badge.svg)](https://github.com/realitycollective/com.realitycollective.service-framework/actions/workflows/development-buildandtestupmrelease.yml)
## Overview
@@ -10,41 +13,25 @@ The Service framework provides a service repository for enabling background serv
- Platform specific operation - choose which platforms your service runs on.
- Zero Latency from Unity operations - services are fully c# based with no Unity overhead.
-- Ability to host several sub-services (data providers) as part of a service, automatically maintained by a parent service and also platform aware.
+- Ability to host several sub-services (service modules) as part of a service, automatically maintained by a parent service and also platform aware.
- Fully configurable with Scriptable profiles - Each service can host a configuration profile to change the behaviour of your service without changing code.
## Requirements
-
-- [Unity 2020.3 and above](https://unity.com/)
+- [Unity 2020.3 or above](https://unity.com/)
- [RealityCollective.Utilities](https://github.com/realitycollective/com.realitycollective.utilities)
### OpenUPM
-
-
-[![openupm](https://img.shields.io/npm/v/com.realitycollective.service-framework?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.realitycollective.service-framework/)
The simplest way to getting started using the utilities package in your project is via OpenUPM. Visit [OpenUPM](https://openupm.com/docs/) to learn more about it. Once you have the OpenUPM CLI set up use the following command to add the package to your project:
```text
-`openupm add com.realitycollective.service-framework`
+ openupm add com.realitycollective.service-framework
```
-> For more details on using [OpenUPM CLI, check the docs here](https://github.com/openupm/openupm-cli#installation).
-
-## Build Status
-
-
-| branch | build status |
-| --- | --- |
-| main | [![main](https://github.com/realitycollective/com.realitycollective.service-framework/actions/workflows/buildupmpackages.yml/badge.svg?branch=main)](https://github.com/realitycollective/com.realitycollective.service-framework/actions/workflows/buildupmpackages.yml) |
-| development | [![development](https://github.com/realitycollective/com.realitycollective.service-framework/actions/workflows/buildupmpackages.yml/badge.svg?branch=development)](https://github.com/realitycollective/com.realitycollective.service-framework/actions/workflows/buildupmpackages.yml) |
-
----
-
## Use cases
-The service framework has been the foundation behind such toolkit's as Microsoft's MRTK and open source projects like the XRTK and newly formed Reality Toolkit. These utilise the framework to enable such use cases as:
+The service framework has been the foundation behind such toolkit's as Microsoft's MRTK and open source projects like the XRTK and newly formed [Reality Toolkit](https://www.realitytoolkit.io/). These utilise the framework to enable such use cases as:
- A platform independent input system - A single service able to route input data from multiple controllers on various platforms, each controller only activates on the platform it was designed for.
- An Authentication service - Able to integrate with multiple authentication providers as needed through a single interface.
@@ -70,7 +57,7 @@ Additionally, the generator can also create additional data providers (sub servi
With your service created, it will need to be registered with an active "Service Manager" in a scene, this can either use the provided "Service Manager Instance" component on a GameObject, or uitilised as a private property on a class of your own.
-> Note, at this time, only a single Service Framework Manager can be active in the scene at a time. If you are intending to use the Framework with toolkit's such as the Reality Toolkit which already has an instance of the Service Framework embedded, then you will need to use the toolkit's endpoints to communicate with the Service Framework.
+> Note, at this time, only a single Service Framework Manager can be active in the scene at a time.
Simply create an empty GameObject and add the **ServiceManagerInstance** component to it to begin. From there it is simply a matter of creating a Profile for the Service Manager and then adding your services to it.
@@ -94,27 +81,9 @@ Alternatively, there are also TryGet versions of the Service endpoints which ret
---
-## Final notes
-
-The Service Framework is robustly tested and confirmed to be working in most versions of Unity, including Unity 2021 LTS. However, it is still classed as a preview while the rest of the Reality Toolkit is going through active development.
-It is being used in production solutions by the Reality Collective team, but it will be up to you as a developer how you choose to consume and operate the framework in your solutions.
-
----
-
## Feedback
-Please feel free to provide feedback via the [Reality Toolkit dev channel here](https://github.com/realitycollective/realitytoolkit.dev/issues), all feedback. suggestions and fixes are welcome.
-
----
-
-## Known Issues
-
-There are some fringe areas of the framework which are still under development and improvement, these include:
-
-- In Unity 2021, the inspector for selecting Service Framework Profiles is a little inconsistent due to 2021 changes. No issues found in Unity 2020 or below
-- The Lookups for Service Types and Data Providers types include all services and providers the toolkit can see.
-- We resolved a critical issue where some data types (such as delegates) can cause Unity to crash when used, this is a known Unity issue and has been logged. Several workarounds have been implemented to handle these edge cases but there could possibly be more on different platforms (because Unity...)
-- More documentation is needed for the Service Framework, including examples (currently the Reality Toolkit is the best set of examples). These will be improved over time.
+Please feel free to provide feedback via the [Reality Toolkit dev channel here](https://github.com/realitycollective/com.realitycollective.service-framework/issues), all feedback. suggestions and fixes are welcome.
---
@@ -129,6 +98,6 @@ There are some fringe areas of the framework which are still under development a
## Raise an Information Request
-If there is anything not mentioned in this document or you simply want to know more, raise an [RFI (Request for Information) request here](https://github.com/realitycollective/realitytoolkit.dev/issues/new?assignees=&labels=question&template=request_for_information.md&title=).
+If there is anything not mentioned in this document or you simply want to know more, raise an [RFI (Request for Information) request here](https://github.com/realitycollective/com.realitycollective.service-framework/issues/new?assignees=&labels=question&template=request_for_information.md).
Or simply [**join us on Discord**](https://discord.gg/YjHAQD2XT8) and come chat about your questions, we would love to hear from you
\ No newline at end of file
diff --git a/Runtime/Definitions/Profiles/BaseServiceProfile.cs b/Runtime/Definitions/Profiles/BaseServiceProfile.cs
index 6da536d..deffe5f 100644
--- a/Runtime/Definitions/Profiles/BaseServiceProfile.cs
+++ b/Runtime/Definitions/Profiles/BaseServiceProfile.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.
using RealityCollective.ServiceFramework.Interfaces;
+using System.Collections.Generic;
using UnityEngine;
namespace RealityCollective.ServiceFramework.Definitions
@@ -71,23 +72,28 @@ internal set
}
}
+ ///
+ /// Adds the to the profile.
+ ///
+ /// The to add.
public void AddConfiguration(IServiceConfiguration configuration)
{
- var newConfigs = new IServiceConfiguration[ServiceConfigurations.Length + 1];
+ // If no configuration is passed to add, do nothing.
+ if (configuration is null)
+ {
+ return;
+ }
- for (int i = 0; i < newConfigs.Length; i++)
+ var serviceConfigurations = new List>();
+
+ // If there are existing Service Configurations, import them.
+ if (ServiceConfigurations != null && ServiceConfigurations.Length > 0)
{
- if (i != newConfigs.Length - 1)
- {
- newConfigs[i] = ServiceConfigurations[i];
- }
- else
- {
- newConfigs[i] = configuration;
- }
+ serviceConfigurations.AddRange(ServiceConfigurations);
}
- ServiceConfigurations = newConfigs;
+ serviceConfigurations.Add(configuration);
+ ServiceConfigurations = serviceConfigurations.ToArray();
}
}
}
\ No newline at end of file
diff --git a/Runtime/Definitions/Profiles/ServiceProvidersProfile.cs b/Runtime/Definitions/Profiles/ServiceProvidersProfile.cs
index 94189ff..957be4a 100644
--- a/Runtime/Definitions/Profiles/ServiceProvidersProfile.cs
+++ b/Runtime/Definitions/Profiles/ServiceProvidersProfile.cs
@@ -7,23 +7,24 @@
namespace RealityCollective.ServiceFramework.Definitions
{
[CreateAssetMenu(menuName = "Reality Collective/Service Framework/Service Providers Profile", fileName = "ServiceProvidersProfile", order = (int)CreateProfileMenuItemIndices.ServiceProviders)]
- public class ServiceProvidersProfile : BaseServiceProfile
+ public class ServiceProvidersProfile : BaseServiceProfile
{
[SerializeField]
- [Tooltip("The service manager will only initialise services in the Editor when it is running\nThe default is to always be active and validating service configuration.")]
+ [Tooltip("The service manager will only initialise services in the Editor when it is running in play mode.\nThe default is to always be active and validating service configuration.")]
private bool initializeOnPlay = false;
///
- /// Configuration of the service manager for initialisation of services on play
+ /// The service manager will only initialise services in the Editor when it is running in play mode.
+ /// The default is to always be active and validating service configuration.
///
public bool InitializeOnPlay => initializeOnPlay;
[SerializeField]
- [Tooltip("Ensure that the Service Manager Instance is not destroyed on scene change")]
+ [Tooltip("Ensure that the Service Manager Instance is not destroyed on scene change.")]
private bool doNotDestroyServiceManagerOnLoad = true;
///
- /// Configuration of the service manager for initialisation of services on play
+ /// Ensure that the Service Manager Instance is not destroyed on scene change.
///
public bool DoNotDestroyServiceManagerOnLoad => doNotDestroyServiceManagerOnLoad;
}
diff --git a/Runtime/Definitions/ServiceConfiguration.cs b/Runtime/Definitions/ServiceConfiguration.cs
index 3a3026e..333a13b 100644
--- a/Runtime/Definitions/ServiceConfiguration.cs
+++ b/Runtime/Definitions/ServiceConfiguration.cs
@@ -45,7 +45,7 @@ public class ServiceConfiguration : IServiceConfiguration
/// The concrete type for the .
/// The simple, human readable name for the .
/// The priority this will be initialized in.
- /// The for .
+ /// The for .
public ServiceConfiguration(SystemType instancedType, string name, uint priority, IReadOnlyList runtimePlatforms, BaseProfile profile)
{
this.instancedType = instancedType;
diff --git a/Runtime/Services/ServiceManager.cs b/Runtime/Services/ServiceManager.cs
index 1050d7c..27fdd05 100644
--- a/Runtime/Services/ServiceManager.cs
+++ b/Runtime/Services/ServiceManager.cs
@@ -184,11 +184,9 @@ public static IReadOnlyList AvailablePlatforms
#region Instance Management
///
- /// Returns the Singleton instance of the classes type.
+ /// Returns the singleton instance of the .
///
- public static ServiceManager Instance => instance;
-
- private static ServiceManager instance;
+ public static ServiceManager Instance { get; private set; }
///
/// Gets whether there is an active of the
@@ -201,6 +199,11 @@ public static IReadOnlyList AvailablePlatforms
///
private readonly object InitializedLock = new object();
+ ///
+ /// The has finished initialzing.
+ ///
+ public static event Action Initialized;
+
///
/// Constructor
/// Each Service Manager MUST have a managed GameObject that can route the MonoBehaviours to, if you do not provide a , then a new will be created for you.
@@ -230,7 +233,7 @@ public ServiceManager(GameObject instanceGameObject = null, ServiceProvidersProf
public void Initialize(GameObject instanceGameObject = null, ServiceProvidersProfile profile = null)
{
- instance = null;
+ Instance = null;
serviceManagerInstanceGuid = Guid.NewGuid();
ServiceManagerInstance serviceManagerInstance;
@@ -255,6 +258,7 @@ public void Initialize(GameObject instanceGameObject = null, ServiceProvidersPro
}
InitializeInstance(profile);
+ Initialized?.Invoke(Instance);
}
private void InitializeInstance(ServiceProvidersProfile profile)
@@ -265,12 +269,12 @@ private void InitializeInstance(ServiceProvidersProfile profile)
ServiceManager.Instance.ServiceManagerInstanceGuid != this.serviceManagerInstanceGuid)
{
Debug.LogWarning($"There are multiple instances of the {nameof(ServiceManager)} in this project, is this expected?");
- Debug.Log($"Instance [{instance.ServiceManagerInstanceGuid}] - This [{this.ServiceManagerInstanceGuid}]");
+ Debug.Log($"Instance [{Instance.ServiceManagerInstanceGuid}] - This [{this.ServiceManagerInstanceGuid}]");
}
if (IsInitialized) { return; }
- instance = this;
+ Instance = this;
activeProfile = profile;
Application.quitting += () =>
@@ -328,14 +332,14 @@ public void AssertIsInitialized()
///
/// Returns whether the instance has been initialized or not.
///
- public bool IsInitialized => instance != null && serviceManagerInstanceGameObject.IsNotNull();
+ public bool IsInitialized => Instance != null && serviceManagerInstanceGameObject.IsNotNull();
///
/// function to determine if the class has been initialized or not.
///
public bool ConfirmInitialized()
{
- var access = instance;
+ var access = Instance;
Debug.Assert(IsInitialized.Equals(access != null));
return IsInitialized;
}
@@ -345,6 +349,20 @@ public bool ConfirmInitialized()
///
public void InitializeServiceManager() => InitializeServiceLocator();
+ ///
+ /// Waits for the to initialize until
+ /// seconds have passed or .
+ ///
+ /// Time to wait in seconds for to become true.
+ public static async Task WaitUntilInitializedAsync(float timeout = 10f)
+ {
+ while (!IsActiveAndInitialized && timeout > 0f)
+ {
+ await Task.Yield();
+ timeout -= Time.deltaTime;
+ }
+ }
+
///
/// Once all services are registered and properties updated, the Service Manager will initialize all active services.
/// This ensures all services can reference each other once started.
@@ -381,7 +399,7 @@ private void InitializeServiceLocator(GameObject instance = null)
Debug.Assert(ActiveServices.Count == 0);
- ClearSystemCache();
+ ClearServiceCache();
if (ActiveProfile?.ServiceConfigurations != null)
{
@@ -482,7 +500,7 @@ internal void OnDisable()
internal void OnDestroy()
{
DestroyAllServices();
- ClearSystemCache();
+ ClearServiceCache();
Dispose();
}
@@ -493,9 +511,9 @@ internal void OnApplicationFocus(bool focus)
// If the Service Manager is not configured, stop.
if (activeProfile == null) { return; }
- foreach (var system in activeServices)
+ foreach (var service in activeServices)
{
- system.Value.OnApplicationFocus(focus);
+ service.Value.OnApplicationFocus(focus);
}
}
@@ -506,9 +524,9 @@ internal void OnApplicationPause(bool pause)
// If the Service Manager is not configured, stop.
if (activeProfile == null) { return; }
- foreach (var system in activeServices)
+ foreach (var service in activeServices)
{
- system.Value.OnApplicationPause(pause);
+ service.Value.OnApplicationPause(pause);
}
}
@@ -767,8 +785,7 @@ public bool TryRegisterService(Type interfaceType, IService serviceInstance)
return false;
}
- // If we have registered at least one event system, we're gonna need the Unity UI event
- // system to be available.
+ // If we have registered at least one event Service, we're gonna need the Unity UI event Service to be available.
if (typeof(IEventService).IsAssignableFrom(interfaceType))
{
EnsureEventSystemSetup();
@@ -817,7 +834,7 @@ public bool TryUnregisterService(T serviceInstance) where T : IService
///
/// Remove services from the Service Manager active service registry for a given type and name
///
- /// The interface type for the system to be removed. E.G. InputSystem, BoundarySystem
+ /// The interface type for the Service to be removed. E.G. InputService, BoundaryService
/// The name of the service to be removed. (Only for runtime services)
private bool TryUnregisterService(Type interfaceType, string serviceName) where T : IService
{
@@ -916,12 +933,12 @@ public T GetService(bool showLogs = true) where T : IService
/// The interface type for the service to be retrieved.
/// The instance of the that is registered.
public async Task GetServiceAsync(int timeout = 10) where T : IService
- => await GetService().WaitUntil(system => system != null, timeout);
+ => await GetService().WaitUntil(service => service != null, timeout);
///
/// Retrieve a from the by type.
///
- /// The interface type for the system to be retrieved.
+ /// The interface type for the Service to be retrieved.
/// Should the logs show when services cannot be found?
/// The instance of the that is registered.
public IService GetService(Type interfaceType, bool showLogs = true)
@@ -930,7 +947,7 @@ public IService GetService(Type interfaceType, bool showLogs = true)
///
/// Retrieve a from the .
///
- /// The interface type for the system to be retrieved.
+ /// The interface type for the Service to be retrieved.
/// Name of the specific service.
/// Should the logs show when services cannot be found?
/// The instance of the that is registered.
@@ -947,7 +964,7 @@ public IService GetServiceByName(string serviceName, bool showLogs = true) wh
///
/// Retrieve a from the .
///
- /// The interface type for the system to be retrieved.
+ /// The interface type for the Service to be retrieved.
/// Name of the specific service.
/// Should the logs show when services cannot be found?
/// The instance of the that is registered.
@@ -1043,7 +1060,7 @@ public bool TryGetServiceByName(Type interfaceType, string serviceName, out ISer
///
/// Retrieve all services from the active service registry for a given type and an optional name
///
- /// The interface type for the system to be retrieved. E.G. IStorageService.
+ /// The interface type for the Service to be retrieved. E.G. IStorageService.
/// An array of services that meet the search criteria
public List GetServices() where T : IService
{
@@ -1053,7 +1070,7 @@ public List GetServices() where T : IService
///
/// Retrieve all services from the active service registry for a given type and an optional name
///
- /// The interface type for the system to be retrieved. E.G. Storage Service.
+ /// The interface type for the Service to be retrieved. E.G. Storage Service.
/// An array of services that meet the search criteria
public List GetServices(Type interfaceType) where T : IService
{
@@ -1063,7 +1080,7 @@ public List GetServices(Type interfaceType) where T : IService
///
/// Retrieve all services from the active service registry for a given type and name
///
- /// The interface type for the system to be retrieved. Storage Service.
+ /// The interface type for the Service to be retrieved. Storage Service.
/// Name of the specific service
/// An array of services that meet the search criteria
public List GetServices(Type interfaceType, string serviceName) where T : IService
@@ -1078,7 +1095,7 @@ public List GetServices(Type interfaceType, string serviceName) where T :
///
/// Retrieve all services from the active service registry for a given type and name
///
- /// The interface type for the system to be retrieved. Storage Service.
+ /// The interface type for the Service to be retrieved. Storage Service.
/// Name of the specific service
/// An array of services that meet the search criteria
public bool TryGetServices(Type interfaceType, string serviceName, ref List services) where T : IService
@@ -1145,12 +1162,12 @@ public void GetAllServices(ref List services)
}
///
- /// Retrieve a cached refernece of an from the .
+ /// Retrieve a cached refernece of an from the .
///
- /// The interface type for the system to be retrieved.
+ /// The interface type for the Service to be retrieved.
/// The instance of the that is registered.
///
- /// Internal function used for high performant systems or components, not to be overused.
+ /// Internal function used for high performant services or components, not to be overused.
///
public T GetServiceCached() where T : IService
{
@@ -1189,7 +1206,7 @@ public T GetServiceCached() where T : IService
///
/// Retrieve a from the .
///
- /// The interface type for the system to be retrieved.
+ /// The interface type for the Service to be retrieved.
/// Optional, time out in seconds to wait before giving up search.
/// The instance of the that is registered.
public async Task GetSystemCachedAsync(int timeout = 10) where T : IService
@@ -1198,8 +1215,8 @@ public async Task GetSystemCachedAsync(int timeout = 10) where T : IServic
///
/// Retrieve a from the .
///
- /// The interface type for the system to be retrieved.
- /// The instance of the system class that is registered.
+ /// The interface type for the Service to be retrieved.
+ /// The instance of the Service class that is registered.
/// Returns true if the was found, otherwise false.
public bool TryGetServiceCached(out T service) where T : IService
{
@@ -1214,7 +1231,7 @@ public bool TryGetServiceCached(out T service) where T : IService
///
/// Enables a services in the active service registry for a given name.
///
- /// The interface type for the system to be enabled. E.G. InputSystem, BoundarySystem
+ /// The interface type for the service to be enabled. E.G. InputService, BoundaryService
///
public void EnableService(string serviceName) where T : IService
{
@@ -1224,7 +1241,7 @@ public void EnableService(string serviceName) where T : IService
///
/// Enable services in the active service registry for a given type
///
- /// The interface type for the system to be enabled. E.G. InputSystem, BoundarySystem
+ /// The interface type for the service to be enabled. E.G. InputService, BoundaryService
public void EnableService() where T : IService
{
EnableAllServicesByTypeAndName(typeof(T), string.Empty);
@@ -1233,7 +1250,7 @@ public void EnableService() where T : IService
///
/// Enable all services in the active service registry for a given type and name
///
- /// The interface type for the system to be enabled. E.G. InputSystem, BoundarySystem
+ /// The interface type for the Service to be enabled. E.G. InputService, BoundaryService
/// Name of the specific service
private void EnableAllServicesByTypeAndName(Type interfaceType, string serviceName)
{
@@ -1265,7 +1282,7 @@ private void EnableAllServicesByTypeAndName(Type interfaceType, string serviceNa
///
/// Disable services in the active service registry for a given type
///
- /// The interface type for the system to be enabled. E.G. InputSystem, BoundarySystem
+ /// The interface type for the service to be enabled. E.G. InputService, BoundaryService
public void DisableService() where T : IService
{
DisableAllServicesByTypeAndName(typeof(T), string.Empty);
@@ -1274,7 +1291,7 @@ public void DisableService() where T : IService
///
/// DDisable services in the active service registry for a given name.
///
- /// The interface type for the system to be enabled. E.G. InputSystem, BoundarySystem
+ /// The interface type for the service to be enabled. E.G. InputService, BoundaryService
/// Name of the specific service
public void DisableService(string serviceName) where T : IService
{
@@ -1284,7 +1301,7 @@ public void DisableService(string serviceName) where T : IService
///
/// Disable all services in the Mixed Reality Toolkit active service registry for a given type and name
///
- /// The interface type for the system to be disabled. E.G. InputSystem, BoundarySystem
+ /// The interface type for the Service to be disabled. E.G. InputService, BoundaryService
/// Name of the specific service
private void DisableAllServicesByTypeAndName(Type interfaceType, string serviceName)
{
@@ -1486,8 +1503,10 @@ public void DestroyAllServices()
// If the Service Manager is not configured, stop.
if (activeProfile == null || activeServices == null || activeServices.Count == 0) { return; }
+ var destroyingActiveServices = activeServices.ToArray();
+
// Destroy all service
- foreach (var service in activeServices)
+ foreach (var service in destroyingActiveServices)
{
try
{
@@ -1500,7 +1519,7 @@ public void DestroyAllServices()
}
// Dispose all service
- foreach (var service in activeServices)
+ foreach (var service in destroyingActiveServices)
{
try
{
@@ -1611,7 +1630,7 @@ void Remove(int index, string msg)
#region Service Utilities
- private string[] ignoredNamespaces = { "System.IDisposable",
+ private string[] ignoredNamespaces = { "Service.IDisposable",
"RealityCollective.ServiceFramework.Interfaces.IService",
"RealityCollective.ServiceFramework.Interfaces.IServiceDataProvider"};
@@ -1712,11 +1731,11 @@ private bool CanGetService(Type interfaceType, string serviceName)
}
///
- /// Try to get the of the
+ /// Try to get the of the
///
/// The profile instance.
/// Optional root profile reference.
- /// True if a type is matched and a valid is found, otherwise false.
+ /// True if a type is matched and a valid is found, otherwise false.
public bool TryGetServiceProfile(out TProfile profile, ServiceProvidersProfile rootProfile = null)
where TService : IService
where TProfile : BaseProfile
@@ -1745,7 +1764,7 @@ public bool TryGetServiceProfile(out TProfile profile, Servi
private readonly Dictionary serviceCache = new Dictionary();
private readonly HashSet searchedServiceTypes = new HashSet();
- private void ClearSystemCache()
+ private void ClearServiceCache()
{
serviceCache.Clear();
searchedServiceTypes.Clear();
@@ -1902,9 +1921,9 @@ public void Dispose()
private void OnDispose(bool finalizing)
{
- if (instance == this)
+ if (Instance == this)
{
- instance = null;
+ Instance = null;
}
}
diff --git a/Runtime/Utilities/ValidateConfiguration.cs b/Runtime/Utilities/ValidateConfiguration.cs
new file mode 100644
index 0000000..e6dbc0d
--- /dev/null
+++ b/Runtime/Utilities/ValidateConfiguration.cs
@@ -0,0 +1,126 @@
+// Copyright (c) Reality Collective. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using RealityCollective.ServiceFramework.Definitions;
+using RealityCollective.ServiceFramework.Services;
+using UnityEngine;
+using RealityCollective.ServiceFramework.Interfaces;
+using System.Text;
+using System;
+
+#if UNITY_EDITOR
+using UnityEditor;
+#endif
+
+namespace RealityCollective.ServiceFramework
+{
+ public static class ValidateConfiguration
+ {
+ private const string IgnoreKey = "_ServiceFramework_Editor_IgnorePrompts";
+
+ ///
+ ///
+ ///
+ /// Array of Data Provider types to validate
+ /// Array of Data Provider default configurations to add if missing
+ /// Unit Test helper, to control whether the UI prompt is offered or not
+ ///
+ public static bool ValidateService(this BaseServiceProfile profile, Type[] providerTypesToValidate, IServiceConfiguration[] providerDefaultConfiguration, bool prompt = true) where T : IService
+ {
+#if UNITY_EDITOR
+ if (Application.isPlaying || EditorPrefs.GetBool(IgnoreKey, false))
+ {
+ return false;
+ }
+#endif //UNITY_EDITOR
+
+ if (ServiceManager.IsActiveAndInitialized && ServiceManager.Instance.HasActiveProfile)
+ {
+ var errorsFound = false;
+
+ if (profile == null)
+ {
+ return false;
+ }
+
+ var registeredConfigurations = profile.ServiceConfigurations;
+
+ if (providerTypesToValidate != null &&
+ providerTypesToValidate.Length > 0)
+ {
+ var typesValidated = new bool[providerTypesToValidate.Length];
+
+ for (int i = 0; i < providerTypesToValidate.Length; i++)
+ {
+ if (providerTypesToValidate[i] == null) { continue; }
+
+ for (var j = 0; j < registeredConfigurations.Length; j++)
+ {
+ var subProfile = registeredConfigurations[j];
+
+ if (subProfile.InstancedType?.Type == providerTypesToValidate[i])
+ {
+ typesValidated[i] = true;
+ }
+ }
+ }
+
+ for (var i = 0; i < typesValidated.Length; i++)
+ {
+ if (!typesValidated[i])
+ {
+ errorsFound = true;
+ }
+ }
+
+ if (errorsFound)
+ {
+ var errorDescription = new StringBuilder();
+ errorDescription.AppendLine($"The following service modules were not found in the current {nameof(ServiceProvidersProfile)}:\n");
+
+ for (int i = 0; i < typesValidated.Length; i++)
+ {
+ if (!typesValidated[i])
+ {
+ errorDescription.AppendLine($" [{providerTypesToValidate[i]}]");
+ }
+ }
+
+ errorDescription.AppendLine($"\nYou can either add this manually in\nInput Profile -> Controller service modules\n or click 'App Provider' to add this automatically");
+#if UNITY_EDITOR
+ if (prompt)
+ {
+ if (EditorUtility.DisplayDialog($"{providerTypesToValidate[0]} provider not found", errorDescription.ToString(), "Ignore", "Add Provider"))
+ {
+ EditorPrefs.SetBool(IgnoreKey, true);
+ }
+ else
+ {
+ for (int i = 0; i < providerTypesToValidate.Length; i++)
+ {
+ if (!typesValidated[i])
+ {
+ profile.AddConfiguration(providerDefaultConfiguration[i]);
+ }
+ }
+
+ return true;
+ }
+ }
+ else
+ {
+ Debug.LogWarning(errorDescription);
+ }
+#endif //UNITY_EDITOR
+ }
+ else
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Utilities/ValidateConfiguration.cs.meta b/Runtime/Utilities/ValidateConfiguration.cs.meta
new file mode 100644
index 0000000..dc6fef4
--- /dev/null
+++ b/Runtime/Utilities/ValidateConfiguration.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5e9f6eef80c119147b78fe3b3f49e581
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/package.json b/package.json
index 288f43b..105cbff 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"Services",
"Extensions"
],
- "version": "1.0.1",
+ "version": "1.0.2-pre.6",
"unity": "2020.3",
"homepage": "https://realitycollective.io",
"bugs": {
@@ -27,6 +27,6 @@
"dependencies": {
"com.unity.editorcoroutines": "1.0.0",
"com.unity.addressables": "1.20.5",
- "com.realitycollective.utilities": "1.0.0"
+ "com.realitycollective.utilities": "1.0.4"
}
}