test
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
[Serializable]
|
||||
internal class PreReleaseHandling
|
||||
{
|
||||
[SerializeReference]
|
||||
PreReleaseHandlingBase[] m_PreReleaseHandlings =
|
||||
{
|
||||
};
|
||||
|
||||
public event Action OnAllChecksFinished;
|
||||
|
||||
public bool IsReady => m_PreReleaseHandlings != null &&
|
||||
Array.TrueForAll(m_PreReleaseHandlings, p => p is {IsReady: true});
|
||||
|
||||
public void CheckForUpdates()
|
||||
{
|
||||
foreach (var package in m_PreReleaseHandlings)
|
||||
{
|
||||
package.OnCheckFinished += OnOnePackageVersionCheckFinished;
|
||||
package.CheckForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
public void PatchPackages(RecommendationViewData toPatch)
|
||||
{
|
||||
foreach (var package in m_PreReleaseHandlings)
|
||||
{
|
||||
package.PatchPackages(toPatch);
|
||||
}
|
||||
}
|
||||
|
||||
public void PatchRecommenderSystemData()
|
||||
{
|
||||
foreach (var package in m_PreReleaseHandlings)
|
||||
{
|
||||
package.PatchRecommenderSystemData();
|
||||
}
|
||||
}
|
||||
|
||||
void OnOnePackageVersionCheckFinished()
|
||||
{
|
||||
var allVersionChecksDone = true;
|
||||
foreach (var package in m_PreReleaseHandlings)
|
||||
{
|
||||
allVersionChecksDone &= package.IsReady;
|
||||
if (package.IsReady)
|
||||
{
|
||||
package.OnCheckFinished -= OnOnePackageVersionCheckFinished;
|
||||
}
|
||||
}
|
||||
|
||||
if (allVersionChecksDone)
|
||||
{
|
||||
OnAllChecksFinished?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal abstract class PreReleaseHandlingBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the versions data is ready to be used.
|
||||
/// </summary>
|
||||
public bool IsReady => m_VersionsInfo != null && !string.IsNullOrEmpty(m_VersionsInfo.latestCompatible);
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when this instance is ready
|
||||
/// </summary>
|
||||
public event Action OnCheckFinished;
|
||||
|
||||
/// <summary>
|
||||
/// The package id e.g. com.unity.netcode
|
||||
/// </summary>
|
||||
public abstract string PackageId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum version that we target.
|
||||
/// </summary>
|
||||
public abstract string MinVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached versions of the package.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
protected VersionsInfo m_VersionsInfo;
|
||||
|
||||
/// <summary>
|
||||
/// version which would be installed by package manager
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
protected string m_DefaultVersion;
|
||||
|
||||
PackageManagement.VersionChecker m_VersionChecker;
|
||||
|
||||
/// <summary>
|
||||
/// Start (online) request to check for available versions.
|
||||
/// </summary>
|
||||
public void CheckForUpdates()
|
||||
{
|
||||
m_VersionChecker = new PackageManagement.VersionChecker(PackageId);
|
||||
m_VersionChecker.OnVersionFound += OnVersionFound;
|
||||
}
|
||||
|
||||
internal string GetPreReleaseVersion(string version, VersionsInfo versionsInfo)
|
||||
{
|
||||
if (version != null && version.StartsWith(MinVersion))
|
||||
return null; // no need for a pre-release version
|
||||
return versionsInfo.latestCompatible.StartsWith(MinVersion) ? versionsInfo.latestCompatible : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patch the recommendation view data directly.
|
||||
/// </summary>
|
||||
/// <param name="toPatch">The view data to patch</param>
|
||||
public abstract void PatchPackages(RecommendationViewData toPatch);
|
||||
|
||||
/// <summary>
|
||||
/// Patch the recommender system data, which will be used for every use of the recommendation.
|
||||
/// </summary>
|
||||
public abstract void PatchRecommenderSystemData();
|
||||
|
||||
protected virtual void BeforeRaisingCheckFinished() { }
|
||||
|
||||
void OnVersionFound(PackageInfo packageInfo)
|
||||
{
|
||||
m_DefaultVersion = packageInfo.version;
|
||||
m_VersionsInfo = packageInfo.versions;
|
||||
if (m_VersionChecker != null) // null observed in tests
|
||||
m_VersionChecker.OnVersionFound -= OnVersionFound;
|
||||
m_VersionChecker = null;
|
||||
BeforeRaisingCheckFinished();
|
||||
OnCheckFinished?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation that fetches unconditionally the version starting with a prefix for a given package, even if it
|
||||
/// is an experimental package
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class SimplePreReleaseHandling : PreReleaseHandlingBase
|
||||
{
|
||||
[SerializeField] string m_MinVersion;
|
||||
[SerializeField] string m_PackageId;
|
||||
|
||||
public override string MinVersion => m_MinVersion;
|
||||
public override string PackageId => m_PackageId;
|
||||
|
||||
public SimplePreReleaseHandling(string packageId, string minVersion)
|
||||
{
|
||||
m_PackageId = packageId;
|
||||
m_MinVersion = minVersion;
|
||||
}
|
||||
|
||||
private SimplePreReleaseHandling() { }
|
||||
|
||||
public override void PatchPackages(RecommendationViewData toPatch)
|
||||
{
|
||||
// Nothing to do, we only patch the package details in the recommender system data
|
||||
}
|
||||
|
||||
public override void PatchRecommenderSystemData()
|
||||
{
|
||||
if (!IsReady) return;
|
||||
|
||||
var allPackages = RecommenderSystemDataObject.instance.RecommenderSystemData.Packages;
|
||||
foreach (var package in allPackages)
|
||||
{
|
||||
if (package.Id == PackageId)
|
||||
{
|
||||
package.PreReleaseVersion = GetPreReleaseVersion(m_DefaultVersion, m_VersionsInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23342257960b4b618e6f7c444137663c
|
||||
timeCreated: 1718177126
|
@@ -0,0 +1,272 @@
|
||||
#if MULTIPLAYER_CENTER_DEV_MODE
|
||||
using System;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
/// <summary>
|
||||
/// This file contains all data relevant for authoring recommendations.
|
||||
/// </summary>
|
||||
|
||||
internal static class Packages
|
||||
{
|
||||
public const string VivoxId = "com.unity.services.vivox";
|
||||
public const string MultiplayerSdkId = "com.unity.services.multiplayer";
|
||||
public const string MultiplayerWidgetsId = "com.unity.multiplayer.widgets";
|
||||
public const string NetcodeForEntitiesId = "com.unity.netcode";
|
||||
public const string NetcodeGameObjectsId = "com.unity.netcode.gameobjects";
|
||||
public const string MultiplayerToolsId = "com.unity.multiplayer.tools";
|
||||
public const string MultiplayerPlayModeId = "com.unity.multiplayer.playmode";
|
||||
public const string CloudCodeId = "com.unity.services.cloudcode";
|
||||
public const string TransportId = "com.unity.transport";
|
||||
public const string DeploymentPackageId = "com.unity.services.deployment";
|
||||
public const string DedicatedServerPackageId = "com.unity.dedicated-server";
|
||||
public const string EntitiesGraphics = "com.unity.entities.graphics";
|
||||
}
|
||||
internal static class Reasons
|
||||
{
|
||||
public const string MultiplayerPlayModeRecommended = "Multiplayer Play Mode enables you to iterate faster by testing locally without the need to create builds.";
|
||||
public const string VivoxRecommended = "Vivox adds real-time communication and is always recommended to boost players engagement.";
|
||||
public const string MultiplayerToolsRecommended = "Tools speed up your development workflow for games using Netcode for GameObjects.";
|
||||
public const string MultiplayerToolsIncompatible = "Multiplayer Tools is only compatible with Netcode for GameObjects.";
|
||||
public const string MultiplayerSdkRecommended = "The Multiplayer Services package makes it easy to connect players together using Unity Gaming Services.";
|
||||
public const string MultiplayerSdkRecommendedNoNetcode = "The Multiplayer Services package makes it easy to connect players together using Unity Gaming Services. This can also be useful in a no-netcode context.";
|
||||
public const string MultiplayerWidgetsRecommended = "Widgets provide pre-implemented building blocks to help you connect players together using services as fast as possible.";
|
||||
public const string MultiplayerWidgetsNotRecommendedBecauseWrongNetcode = "The Multiplayer Widgets package is currently only recommended with Netcode for GameObjects and Netcode for Entities.";
|
||||
public const string DedicatedServerPackageRecommended = "The Dedicated Server package, which includes Content Selection, helps with the development for the dedicated server platforms.";
|
||||
public const string DedicatedServerPackageNotRecommended = "The Dedicated Server package is only recommended when you have a dedicated server architecture and use Netcode for Gameobjects.";
|
||||
public const string DedicatedServerPackageNotRecommendedN4E = "The Dedicated Server package is currently not recommended when using Netcode for Entities.";
|
||||
public const string DistributedAuthorityIncompatible = "Distributed Authority is only available when using Netcode for GameObjects.";
|
||||
public const string DistributedAuthorityNotAvailable = "Distributed Authority is not available yet.";
|
||||
public const string EntitiesGraphicsRecommended = "While you do not need Entities Graphics to use Netcode for Entities, it will make it easier for you to get started";
|
||||
public const string EntitiesGraphicsNotRecommended = "You would typically only use Entities Graphics with Netcode for Entities";
|
||||
public const string TransportRecommended = "Unity Transport is the low-level interface for connecting and sending data through a network, and can be the basis of your custom netcode solution.";
|
||||
public const string DeploymentRecommended = "The deployment package is necessary to upload your C# Cloud Code scripts.";
|
||||
}
|
||||
|
||||
internal static class DocLinks
|
||||
{
|
||||
public const string DedicatedServer = "https://docs-multiplayer.unity3d.com/netcode/current/terms-concepts/network-topologies/#dedicated-game-server";
|
||||
public const string ListenServer = "https://docs-multiplayer.unity3d.com/netcode/current/terms-concepts/network-topologies/#client-hosted-listen-server";
|
||||
public const string NetcodeForGameObjects = "https://docs-multiplayer.unity3d.com/netcode/current/about/";
|
||||
public const string NetcodeForEntities = "https://docs.unity3d.com/Packages/com.unity.netcode@latest";
|
||||
public const string Vivox = "https://docs.unity.com/ugs/en-us/manual/vivox-unity/manual/Unity/Unity";
|
||||
public const string MultiplayerTools = "https://docs-multiplayer.unity3d.com/tools/current/about/";
|
||||
public const string MultiplayerPlayMode = "https://docs-multiplayer.unity3d.com/mppm/current/about/";
|
||||
public const string MultiplayerSdk = "https://docs.unity3d.com/Packages/com.unity.services.multiplayer@latest";
|
||||
public const string MultiplayerWidgets = "https://docs.unity3d.com/Packages/com.unity.multiplayer.widgets@latest";
|
||||
public const string CloudCode = "https://docs.unity.com/ugs/manual/cloud-code/manual";
|
||||
public const string Transport = "https://docs.unity3d.com/Packages/com.unity.transport@latest";
|
||||
public const string DedicatedServerPackage = "https://docs.unity3d.com/Packages/com.unity.dedicated-server@latest";
|
||||
public const string DistributedAuthority = "https://docs-multiplayer.unity3d.com/netcode/current/terms-concepts/distributed-authority/";
|
||||
public const string EntitiesGraphics = "https://docs.unity3d.com/Packages/com.unity.entities.graphics@latest";
|
||||
public const string DeploymentPackage = "https://docs.unity3d.com/Packages/com.unity.services.deployment@latest";
|
||||
}
|
||||
|
||||
internal static class SolutionDescriptionAndReason
|
||||
{
|
||||
public const string DistributedAuthority = "The authority over the game will be distributed across different players. This hosting model [dynamic].";
|
||||
public const string DedicatedServer = "A dedicated server has authority over the game logic. This hosting model [dynamic].";
|
||||
public const string ListenServer = "A player will be the host of your game. This hosting model [dynamic].";
|
||||
public const string NetcodeForGameObjects = "Netcode for GameObject is a high-level networking library built for GameObjects to abstract networking logic. It is made for simplicity. It [dynamic].";
|
||||
public const string NetcodeForEntities = "Netcode for Entities is a multiplayer solution with server authority and client prediction. It is made for performance. It [dynamic].";
|
||||
public const string CloudCode = "When gameplay does not require a synchronous multiplayer experience, Cloud Code allows to run player interaction logic directly on the backend side. It [dynamic].";
|
||||
public const string CustomNetcode = "Custom or third-party netcode, not provided by Unity. It [dynamic].";
|
||||
public const string NoNetcode = "Your game doesn't require realtime synchronization. No Netcode [dynamic].";
|
||||
}
|
||||
|
||||
internal static class CatchPhrases
|
||||
{
|
||||
public const string NetcodeForGameObjects = "Multiplayer synchronization for gameplay based on GameObjects.";
|
||||
public const string NetcodeForEntities = "Multiplayer synchronization for gameplay based on Entity Component System.";
|
||||
public const string Vivox = "Connect players through voice and text chat.";
|
||||
public const string MultiplayerTools = "Debug and optimize your multiplayer gameplay.";
|
||||
public const string MultiplayerPlayMode = "Test multiplayer gameplay in separated processes from the same project.";
|
||||
public const string MultiplayerSdk = "Connect players together in sessions for lobby, matchmaker, etc.";
|
||||
public const string MultiplayerWidgets = "Experiment rapidly with multiplayer services.";
|
||||
public const string DedicatedServerPackage = "Streamline dedicated server builds.";
|
||||
public const string CloudCode = "Run game logic as serverless functions.";
|
||||
public const string EntitiesGraphics = "Optimized rendering for Entity Component System.";
|
||||
public const string DeploymentPackage = "Deploy assets to Unity Gaming Services from the Editor.";
|
||||
public const string Transport = "Low-level networking communication layer.";
|
||||
}
|
||||
|
||||
internal static class Titles
|
||||
{
|
||||
public const string DedicatedServer = "Dedicated Server";
|
||||
public const string ListenServer = "Client Hosted";
|
||||
public const string NetcodeForGameObjects = "Netcode for GameObjects";
|
||||
public const string NetcodeForEntities = "Netcode for Entities";
|
||||
public const string Vivox = "Voice/Text chat (Vivox)";
|
||||
public const string MultiplayerTools = "Multiplayer Tools";
|
||||
public const string MultiplayerPlayMode = "Multiplayer Play Mode";
|
||||
public const string NoUnityNetcode = "No Netcode";
|
||||
public const string MultiplayerSdk = "Multiplayer Services";
|
||||
public const string MultiplayerWidgets = "Multiplayer Widgets";
|
||||
public const string CloudCode = "Cloud Code";
|
||||
public const string CustomNetcode = "Custom or Third-party Netcode";
|
||||
public const string Transport = "Transport";
|
||||
public const string DedicatedServerPackage = "Dedicated Server Package";
|
||||
public const string DeploymentPackage = "Deployment Package";
|
||||
public const string EntitiesGraphics = "Entities Graphics";
|
||||
public const string DistributedAuthority = "Distributed Authority";
|
||||
}
|
||||
|
||||
static class RecommendationAssetUtils
|
||||
{
|
||||
public static RecommenderSystemData PopulateDefaultRecommendationData()
|
||||
{
|
||||
var data = new RecommenderSystemData();
|
||||
data.TargetUnityVersion = UnityEngine.Application.unityVersion;
|
||||
data.RecommendedSolutions = new RecommendedSolution[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Type = PossibleSolution.LS,
|
||||
Title = Titles.ListenServer,
|
||||
DocUrl = DocLinks.ListenServer,
|
||||
ShortDescription = SolutionDescriptionAndReason.ListenServer,
|
||||
RecommendedPackages = new RecommendedPackage[]
|
||||
{
|
||||
new(Packages.DedicatedServerPackageId, RecommendationType.Incompatible, Reasons.DedicatedServerPackageNotRecommended),
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Type = PossibleSolution.DS,
|
||||
Title = Titles.DedicatedServer,
|
||||
DocUrl = DocLinks.DedicatedServer,
|
||||
ShortDescription = SolutionDescriptionAndReason.DedicatedServer,
|
||||
RecommendedPackages = new RecommendedPackage[]
|
||||
{
|
||||
}
|
||||
},
|
||||
|
||||
new()
|
||||
{
|
||||
Type = PossibleSolution.DA,
|
||||
Title = Titles.DistributedAuthority,
|
||||
DocUrl = DocLinks.DistributedAuthority,
|
||||
ShortDescription = SolutionDescriptionAndReason.DistributedAuthority,
|
||||
RecommendedPackages = new RecommendedPackage[]
|
||||
{
|
||||
new(Packages.MultiplayerSdkId, RecommendationType.HostingFeatured, "Distributed Authority needs the Multiplayer Services package to work."),
|
||||
new(Packages.DedicatedServerPackageId, RecommendationType.Incompatible, Reasons.DedicatedServerPackageNotRecommended),
|
||||
}
|
||||
},
|
||||
|
||||
new()
|
||||
{
|
||||
Type = PossibleSolution.CloudCode,
|
||||
Title = Titles.CloudCode,
|
||||
MainPackageId = Packages.CloudCodeId,
|
||||
ShortDescription = SolutionDescriptionAndReason.CloudCode,
|
||||
DocUrl = DocLinks.CloudCode,
|
||||
|
||||
RecommendedPackages = new RecommendedPackage[]
|
||||
{
|
||||
new(Packages.DedicatedServerPackageId, RecommendationType.Incompatible, Reasons.DedicatedServerPackageNotRecommended),
|
||||
new(Packages.DeploymentPackageId, RecommendationType.HostingFeatured, Reasons.DeploymentRecommended),
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Type = PossibleSolution.NGO,
|
||||
Title = Titles.NetcodeForGameObjects,
|
||||
MainPackageId = Packages.NetcodeGameObjectsId,
|
||||
ShortDescription = SolutionDescriptionAndReason.NetcodeForGameObjects,
|
||||
RecommendedPackages = new RecommendedPackage[]
|
||||
{
|
||||
new(Packages.TransportId, RecommendationType.Hidden, null),
|
||||
new(Packages.DeploymentPackageId, RecommendationType.Hidden, null),
|
||||
new(Packages.MultiplayerToolsId, RecommendationType.NetcodeFeatured, Reasons.MultiplayerToolsRecommended),
|
||||
new(Packages.DedicatedServerPackageId, RecommendationType.HostingFeatured, Reasons.DedicatedServerPackageRecommended ),
|
||||
new(Packages.EntitiesGraphics, RecommendationType.Hidden, Reasons.EntitiesGraphicsNotRecommended),
|
||||
new(Packages.VivoxId, RecommendationType.OptionalStandard, Reasons.VivoxRecommended),
|
||||
new(Packages.MultiplayerSdkId, RecommendationType.OptionalStandard, Reasons.MultiplayerSdkRecommended),
|
||||
new(Packages.MultiplayerWidgetsId, RecommendationType.OptionalStandard, Reasons.MultiplayerWidgetsRecommended),
|
||||
new(Packages.MultiplayerPlayModeId, RecommendationType.OptionalStandard, Reasons.MultiplayerPlayModeRecommended),
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Type = PossibleSolution.N4E,
|
||||
Title = Titles.NetcodeForEntities,
|
||||
MainPackageId = Packages.NetcodeForEntitiesId,
|
||||
ShortDescription = SolutionDescriptionAndReason.NetcodeForEntities,
|
||||
RecommendedPackages = new RecommendedPackage[]
|
||||
{
|
||||
new(Packages.TransportId, RecommendationType.Hidden, null),
|
||||
new(Packages.DeploymentPackageId, RecommendationType.Hidden, null),
|
||||
new(Packages.EntitiesGraphics, RecommendationType.NetcodeFeatured, Reasons.EntitiesGraphicsRecommended),
|
||||
new(Packages.VivoxId, RecommendationType.OptionalStandard, Reasons.VivoxRecommended),
|
||||
new(Packages.MultiplayerSdkId, RecommendationType.OptionalStandard, Reasons.MultiplayerSdkRecommended),
|
||||
new(Packages.MultiplayerWidgetsId, RecommendationType.OptionalStandard, Reasons.MultiplayerWidgetsRecommended),
|
||||
new(Packages.MultiplayerToolsId, RecommendationType.Incompatible, Reasons.MultiplayerToolsIncompatible),
|
||||
new(Packages.MultiplayerPlayModeId, RecommendationType.OptionalStandard, Reasons.MultiplayerPlayModeRecommended),
|
||||
new(Packages.DedicatedServerPackageId, RecommendationType.NotRecommended, Reasons.DedicatedServerPackageNotRecommended)
|
||||
},
|
||||
IncompatibleSolutions = new IncompatibleSolution[]{new(PossibleSolution.DA, Reasons.DistributedAuthorityIncompatible)}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Type = PossibleSolution.NoNetcode,
|
||||
Title = Titles.NoUnityNetcode,
|
||||
DocUrl = null,
|
||||
MainPackageId = null,
|
||||
ShortDescription = SolutionDescriptionAndReason.NoNetcode,
|
||||
RecommendedPackages = new RecommendedPackage[]
|
||||
{
|
||||
new(Packages.TransportId, RecommendationType.Hidden, null),
|
||||
new(Packages.DeploymentPackageId, RecommendationType.Hidden, null),
|
||||
new(Packages.MultiplayerToolsId, RecommendationType.Incompatible, Reasons.MultiplayerToolsIncompatible),
|
||||
new(Packages.EntitiesGraphics, RecommendationType.Hidden, Reasons.EntitiesGraphicsNotRecommended),
|
||||
new(Packages.VivoxId, RecommendationType.OptionalStandard, Reasons.VivoxRecommended),
|
||||
new(Packages.MultiplayerSdkId, RecommendationType.OptionalStandard, Reasons.MultiplayerSdkRecommendedNoNetcode),
|
||||
new(Packages.MultiplayerWidgetsId, RecommendationType.NotRecommended, Reasons.MultiplayerWidgetsNotRecommendedBecauseWrongNetcode),
|
||||
new(Packages.MultiplayerPlayModeId, RecommendationType.OptionalStandard, Reasons.MultiplayerPlayModeRecommended),
|
||||
new(Packages.DedicatedServerPackageId, RecommendationType.NotRecommended, Reasons.DedicatedServerPackageNotRecommended),
|
||||
},
|
||||
IncompatibleSolutions = new IncompatibleSolution[]{new(PossibleSolution.DA, Reasons.DistributedAuthorityIncompatible)}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Type = PossibleSolution.CustomNetcode,
|
||||
Title = Titles.CustomNetcode,
|
||||
MainPackageId = null,
|
||||
ShortDescription = SolutionDescriptionAndReason.CustomNetcode,
|
||||
RecommendedPackages = new RecommendedPackage[]
|
||||
{
|
||||
new(Packages.TransportId, RecommendationType.NetcodeFeatured, Reasons.TransportRecommended),
|
||||
new(Packages.EntitiesGraphics, RecommendationType.Hidden, Reasons.EntitiesGraphicsNotRecommended),
|
||||
new(Packages.DeploymentPackageId, RecommendationType.Hidden, null),
|
||||
new(Packages.VivoxId, RecommendationType.OptionalStandard, Reasons.VivoxRecommended),
|
||||
new(Packages.MultiplayerSdkId, RecommendationType.OptionalStandard, Reasons.MultiplayerSdkRecommended),
|
||||
new(Packages.MultiplayerWidgetsId, RecommendationType.NotRecommended, Reasons.MultiplayerWidgetsNotRecommendedBecauseWrongNetcode),
|
||||
new(Packages.MultiplayerToolsId, RecommendationType.Incompatible, Reasons.MultiplayerToolsIncompatible),
|
||||
new(Packages.MultiplayerPlayModeId, RecommendationType.OptionalStandard, Reasons.MultiplayerPlayModeRecommended),
|
||||
new(Packages.DedicatedServerPackageId, RecommendationType.NotRecommended, Reasons.DedicatedServerPackageNotRecommendedN4E),
|
||||
},
|
||||
IncompatibleSolutions = new IncompatibleSolution[]{new(PossibleSolution.DA, Reasons.DistributedAuthorityIncompatible)}
|
||||
}
|
||||
};
|
||||
data.Packages = new PackageDetails[]
|
||||
{
|
||||
new(Packages.VivoxId, Titles.Vivox, CatchPhrases.Vivox, DocLinks.Vivox),
|
||||
new(Packages.MultiplayerSdkId, Titles.MultiplayerSdk, CatchPhrases.MultiplayerSdk, DocLinks.MultiplayerSdk),
|
||||
new(Packages.MultiplayerWidgetsId, Titles.MultiplayerWidgets, CatchPhrases.MultiplayerWidgets, DocLinks.MultiplayerWidgets),
|
||||
new(Packages.NetcodeForEntitiesId, Titles.NetcodeForEntities, CatchPhrases.NetcodeForEntities, DocLinks.NetcodeForEntities),
|
||||
new(Packages.NetcodeGameObjectsId, Titles.NetcodeForGameObjects, CatchPhrases.NetcodeForGameObjects, DocLinks.NetcodeForGameObjects),
|
||||
new(Packages.MultiplayerToolsId, Titles.MultiplayerTools, CatchPhrases.MultiplayerTools, DocLinks.MultiplayerTools),
|
||||
new(Packages.MultiplayerPlayModeId, Titles.MultiplayerPlayMode, CatchPhrases.MultiplayerPlayMode, DocLinks.MultiplayerPlayMode),
|
||||
new(Packages.CloudCodeId, Titles.CloudCode, CatchPhrases.CloudCode, DocLinks.CloudCode),
|
||||
new(Packages.TransportId, Titles.Transport, CatchPhrases.Transport, DocLinks.Transport),
|
||||
new(Packages.DedicatedServerPackageId, Titles.DedicatedServerPackage, CatchPhrases.DedicatedServerPackage, DocLinks.DedicatedServerPackage),
|
||||
new(Packages.EntitiesGraphics, Titles.EntitiesGraphics, CatchPhrases.EntitiesGraphics, DocLinks.EntitiesGraphics),
|
||||
new(Packages.DeploymentPackageId, Titles.DeploymentPackage, CatchPhrases.DeploymentPackage, DocLinks.DeploymentPackage)
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71b3035ad4d343cbbe033e93369d187e
|
||||
timeCreated: 1711113941
|
@@ -0,0 +1,316 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &1
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 53
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: b6a02e450cd04bb781e7728f2056251c, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RecommenderSystemData:
|
||||
TargetUnityVersion: 6000.0.13f1
|
||||
RecommendedSolutions:
|
||||
- Type: 2
|
||||
Title: Client Hosted
|
||||
MainPackageId:
|
||||
DocUrl: https://docs-multiplayer.unity3d.com/netcode/current/terms-concepts/network-topologies/#client-hosted-listen-server
|
||||
ShortDescription: A player will be the host of your game. This hosting model
|
||||
[dynamic].
|
||||
RecommendedPackages:
|
||||
- PackageId: com.unity.dedicated-server
|
||||
Type: 7
|
||||
Reason: The Dedicated Server package is only recommended when you have a
|
||||
dedicated server architecture and use Netcode for Gameobjects.
|
||||
IncompatibleSolutions: []
|
||||
- Type: 3
|
||||
Title: Dedicated Server
|
||||
MainPackageId:
|
||||
DocUrl: https://docs-multiplayer.unity3d.com/netcode/current/terms-concepts/network-topologies/#dedicated-game-server
|
||||
ShortDescription: A dedicated server has authority over the game logic. This
|
||||
hosting model [dynamic].
|
||||
RecommendedPackages: []
|
||||
IncompatibleSolutions: []
|
||||
- Type: 4
|
||||
Title: Distributed Authority
|
||||
MainPackageId:
|
||||
DocUrl: https://docs-multiplayer.unity3d.com/netcode/current/terms-concepts/distributed-authority/
|
||||
ShortDescription: The authority over the game will be distributed across different
|
||||
players. This hosting model [dynamic].
|
||||
RecommendedPackages:
|
||||
- PackageId: com.unity.services.multiplayer
|
||||
Type: 4
|
||||
Reason: Distributed Authority needs the Multiplayer Services package to work.
|
||||
- PackageId: com.unity.dedicated-server
|
||||
Type: 7
|
||||
Reason: The Dedicated Server package is only recommended when you have a
|
||||
dedicated server architecture and use Netcode for Gameobjects.
|
||||
IncompatibleSolutions: []
|
||||
- Type: 7
|
||||
Title: Cloud Code
|
||||
MainPackageId: com.unity.services.cloudcode
|
||||
DocUrl: https://docs.unity.com/ugs/manual/cloud-code/manual
|
||||
ShortDescription: When gameplay does not require a synchronous multiplayer
|
||||
experience, Cloud Code allows to run player interaction logic directly on
|
||||
the backend side. It [dynamic].
|
||||
RecommendedPackages:
|
||||
- PackageId: com.unity.dedicated-server
|
||||
Type: 7
|
||||
Reason: The Dedicated Server package is only recommended when you have a
|
||||
dedicated server architecture and use Netcode for Gameobjects.
|
||||
- PackageId: com.unity.services.deployment
|
||||
Type: 4
|
||||
Reason: The deployment package is necessary to upload your C# Cloud Code
|
||||
scripts.
|
||||
IncompatibleSolutions: []
|
||||
- Type: 0
|
||||
Title: Netcode for GameObjects
|
||||
MainPackageId: com.unity.netcode.gameobjects
|
||||
DocUrl:
|
||||
ShortDescription: Netcode for GameObject is a high-level networking library
|
||||
built for GameObjects to abstract networking logic. It is made for simplicity.
|
||||
It [dynamic].
|
||||
RecommendedPackages:
|
||||
- PackageId: com.unity.transport
|
||||
Type: 8
|
||||
Reason:
|
||||
- PackageId: com.unity.services.deployment
|
||||
Type: 8
|
||||
Reason:
|
||||
- PackageId: com.unity.multiplayer.tools
|
||||
Type: 3
|
||||
Reason: Tools speed up your development workflow for games using Netcode
|
||||
for GameObjects.
|
||||
- PackageId: com.unity.dedicated-server
|
||||
Type: 4
|
||||
Reason: The Dedicated Server package, which includes Content Selection, helps
|
||||
with the development for the dedicated server platforms.
|
||||
- PackageId: com.unity.entities.graphics
|
||||
Type: 8
|
||||
Reason: You would typically only use Entities Graphics with Netcode for Entities
|
||||
- PackageId: com.unity.services.vivox
|
||||
Type: 5
|
||||
Reason: Vivox adds real-time communication and is always recommended to boost
|
||||
players engagement.
|
||||
- PackageId: com.unity.services.multiplayer
|
||||
Type: 5
|
||||
Reason: The Multiplayer Services package makes it easy to connect players
|
||||
together using Unity Gaming Services.
|
||||
- PackageId: com.unity.multiplayer.widgets
|
||||
Type: 5
|
||||
Reason: Widgets provide pre-implemented building blocks to help you connect
|
||||
players together using services as fast as possible.
|
||||
- PackageId: com.unity.multiplayer.playmode
|
||||
Type: 5
|
||||
Reason: Multiplayer Play Mode enables you to iterate faster by testing locally
|
||||
without the need to create builds.
|
||||
IncompatibleSolutions: []
|
||||
- Type: 1
|
||||
Title: Netcode for Entities
|
||||
MainPackageId: com.unity.netcode
|
||||
DocUrl:
|
||||
ShortDescription: Netcode for Entities is a multiplayer solution with server
|
||||
authority and client prediction. It [dynamic].
|
||||
RecommendedPackages:
|
||||
- PackageId: com.unity.transport
|
||||
Type: 8
|
||||
Reason:
|
||||
- PackageId: com.unity.services.deployment
|
||||
Type: 8
|
||||
Reason:
|
||||
- PackageId: com.unity.entities.graphics
|
||||
Type: 3
|
||||
Reason: While you do not need Entities Graphics to use Netcode for Entities,
|
||||
it will make it easier for you to get started
|
||||
- PackageId: com.unity.services.vivox
|
||||
Type: 5
|
||||
Reason: Vivox adds real-time communication and is always recommended to boost
|
||||
players engagement.
|
||||
- PackageId: com.unity.services.multiplayer
|
||||
Type: 5
|
||||
Reason: The Multiplayer Services package makes it easy to connect players
|
||||
together using Unity Gaming Services.
|
||||
- PackageId: com.unity.multiplayer.widgets
|
||||
Type: 5
|
||||
Reason: Widgets provide pre-implemented building blocks to help you connect
|
||||
players together using services as fast as possible.
|
||||
- PackageId: com.unity.multiplayer.tools
|
||||
Type: 7
|
||||
Reason: Multiplayer Tools is only compatible with Netcode for GameObjects.
|
||||
- PackageId: com.unity.multiplayer.playmode
|
||||
Type: 5
|
||||
Reason: Multiplayer Play Mode enables you to iterate faster by testing locally
|
||||
without the need to create builds.
|
||||
- PackageId: com.unity.dedicated-server
|
||||
Type: 6
|
||||
Reason: The Dedicated Server package is only recommended when you have a
|
||||
dedicated server architecture and use Netcode for Gameobjects.
|
||||
IncompatibleSolutions:
|
||||
- Solution: 4
|
||||
Reason: Distributed Authority is only available when using Netcode for GameObjects.
|
||||
- Type: 6
|
||||
Title: No Netcode
|
||||
MainPackageId:
|
||||
DocUrl:
|
||||
ShortDescription: Your game doesn't require realtime synchronization. No Netcode
|
||||
[dynamic].
|
||||
RecommendedPackages:
|
||||
- PackageId: com.unity.transport
|
||||
Type: 8
|
||||
Reason:
|
||||
- PackageId: com.unity.services.deployment
|
||||
Type: 8
|
||||
Reason:
|
||||
- PackageId: com.unity.multiplayer.tools
|
||||
Type: 7
|
||||
Reason: Multiplayer Tools is only compatible with Netcode for GameObjects.
|
||||
- PackageId: com.unity.entities.graphics
|
||||
Type: 8
|
||||
Reason: You would typically only use Entities Graphics with Netcode for Entities
|
||||
- PackageId: com.unity.services.vivox
|
||||
Type: 5
|
||||
Reason: Vivox adds real-time communication and is always recommended to boost
|
||||
players engagement.
|
||||
- PackageId: com.unity.services.multiplayer
|
||||
Type: 5
|
||||
Reason: The Multiplayer Services package makes it easy to connect players
|
||||
together using Unity Gaming Services. This can also be useful in a no-netcode
|
||||
context.
|
||||
- PackageId: com.unity.multiplayer.widgets
|
||||
Type: 6
|
||||
Reason: The Multiplayer Widgets package is currently only recommended with
|
||||
Netcode for GameObjects and Netcode for Entities.
|
||||
- PackageId: com.unity.multiplayer.playmode
|
||||
Type: 5
|
||||
Reason: Multiplayer Play Mode enables you to iterate faster by testing locally
|
||||
without the need to create builds.
|
||||
- PackageId: com.unity.dedicated-server
|
||||
Type: 6
|
||||
Reason: The Dedicated Server package is only recommended when you have a
|
||||
dedicated server architecture and use Netcode for Gameobjects.
|
||||
IncompatibleSolutions:
|
||||
- Solution: 4
|
||||
Reason: Distributed Authority is only available when using Netcode for GameObjects.
|
||||
- Type: 5
|
||||
Title: Custom or Third-party Netcode
|
||||
MainPackageId:
|
||||
DocUrl:
|
||||
ShortDescription: Custom or third-party netcode, not provided by Unity. It
|
||||
[dynamic].
|
||||
RecommendedPackages:
|
||||
- PackageId: com.unity.transport
|
||||
Type: 3
|
||||
Reason: Unity Transport is the low-level interface for connecting and sending
|
||||
data through a network, and can be the basis of your custom netcode solution.
|
||||
- PackageId: com.unity.entities.graphics
|
||||
Type: 8
|
||||
Reason: You would typically only use Entities Graphics with Netcode for Entities
|
||||
- PackageId: com.unity.services.deployment
|
||||
Type: 8
|
||||
Reason:
|
||||
- PackageId: com.unity.services.vivox
|
||||
Type: 5
|
||||
Reason: Vivox adds real-time communication and is always recommended to boost
|
||||
players engagement.
|
||||
- PackageId: com.unity.services.multiplayer
|
||||
Type: 5
|
||||
Reason: The Multiplayer Services package makes it easy to connect players
|
||||
together using Unity Gaming Services.
|
||||
- PackageId: com.unity.multiplayer.widgets
|
||||
Type: 6
|
||||
Reason: The Multiplayer Widgets package is currently only recommended with
|
||||
Netcode for GameObjects and Netcode for Entities.
|
||||
- PackageId: com.unity.multiplayer.tools
|
||||
Type: 7
|
||||
Reason: Multiplayer Tools is only compatible with Netcode for GameObjects.
|
||||
- PackageId: com.unity.multiplayer.playmode
|
||||
Type: 5
|
||||
Reason: Multiplayer Play Mode enables you to iterate faster by testing locally
|
||||
without the need to create builds.
|
||||
- PackageId: com.unity.dedicated-server
|
||||
Type: 6
|
||||
Reason: The Dedicated Server package is currently not recommended when using
|
||||
Netcode for Entities.
|
||||
IncompatibleSolutions:
|
||||
- Solution: 4
|
||||
Reason: Distributed Authority is only available when using Netcode for GameObjects.
|
||||
Packages:
|
||||
- Id: com.unity.services.vivox
|
||||
Name: Voice/Text chat (Vivox)
|
||||
ShortDescription: Connect players through voice and text chat.
|
||||
DocsUrl: https://docs.unity.com/ugs/en-us/manual/vivox-unity/manual/Unity/Unity
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.services.multiplayer
|
||||
Name: Multiplayer Services
|
||||
ShortDescription: Connect players together in sessions for lobby, matchmaker,
|
||||
etc.
|
||||
DocsUrl: https://docs.unity3d.com/Packages/com.unity.services.multiplayer@latest
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.multiplayer.widgets
|
||||
Name: Multiplayer Widgets
|
||||
ShortDescription: Experiment rapidly with multiplayer services.
|
||||
DocsUrl: https://docs.unity3d.com/Packages/com.unity.multiplayer.widgets@latest
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.netcode
|
||||
Name: Netcode for Entities
|
||||
ShortDescription: Multiplayer synchronization for gameplay based on Entity
|
||||
Component System.
|
||||
DocsUrl: https://docs.unity3d.com/Packages/com.unity.netcode@latest
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.netcode.gameobjects
|
||||
Name: Netcode for GameObjects
|
||||
ShortDescription: Multiplayer synchronization for gameplay based on GameObjects.
|
||||
DocsUrl: https://docs-multiplayer.unity3d.com/netcode/current/about/
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.multiplayer.tools
|
||||
Name: Multiplayer Tools
|
||||
ShortDescription: Debug and optimize your multiplayer gameplay.
|
||||
DocsUrl: https://docs-multiplayer.unity3d.com/tools/current/about/
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.multiplayer.playmode
|
||||
Name: Multiplayer Play Mode
|
||||
ShortDescription: Test multiplayer gameplay in separated processes from the
|
||||
same project.
|
||||
DocsUrl: https://docs-multiplayer.unity3d.com/mppm/current/about/
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.services.cloudcode
|
||||
Name: Cloud Code
|
||||
ShortDescription: Run game logic as serverless functions.
|
||||
DocsUrl: https://docs.unity.com/ugs/manual/cloud-code/manual
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.transport
|
||||
Name: Transport
|
||||
ShortDescription: Low-level networking communication layer.
|
||||
DocsUrl: https://docs.unity3d.com/Packages/com.unity.transport@latest
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.dedicated-server
|
||||
Name: Dedicated Server Package
|
||||
ShortDescription: Streamline dedicated server builds.
|
||||
DocsUrl: https://docs.unity3d.com/Packages/com.unity.dedicated-server@latest
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.entities.graphics
|
||||
Name: Entities Graphics
|
||||
ShortDescription: Optimized rendering for Entity Component System.
|
||||
DocsUrl: https://docs.unity3d.com/Packages/com.unity.entities.graphics@latest
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
||||
- Id: com.unity.services.deployment
|
||||
Name: Deployment Package
|
||||
ShortDescription: Deploy assets to Unity Gaming Services from the Editor.
|
||||
DocsUrl: https://docs.unity3d.com/Packages/com.unity.services.deployment@latest
|
||||
AdditionalPackages: []
|
||||
PreReleaseVersion:
|
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b66d076cdcfe3b14388de66307a0e7ff
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
/// <summary>
|
||||
/// Types of recommendation that are both used in ViewData and in the ground truth data for the recommendation.
|
||||
/// Architecture choices must be made. Packages are typically optional.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal enum RecommendationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalid value, indicates error.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Featured option (e.g. NGO if NGO is the recommended architecture)
|
||||
/// Note that in the case of architecture choice, the user should select something.
|
||||
/// </summary>
|
||||
MainArchitectureChoice,
|
||||
|
||||
/// <summary>
|
||||
/// Non-featured option (e.g. N4E if NGO is the recommended architecture)
|
||||
/// </summary>
|
||||
SecondArchitectureChoice,
|
||||
|
||||
/// <summary>
|
||||
/// Associated feature in the Netcode section
|
||||
/// </summary>
|
||||
NetcodeFeatured,
|
||||
|
||||
/// <summary>
|
||||
/// Associated feature in the Hosting Model section
|
||||
/// </summary>
|
||||
HostingFeatured,
|
||||
|
||||
/// <summary>
|
||||
/// Optional but not highlighted
|
||||
/// </summary>
|
||||
OptionalStandard,
|
||||
|
||||
/// <summary>
|
||||
/// Not recommended, but not incompatible with the user intent.
|
||||
/// </summary>
|
||||
NotRecommended,
|
||||
|
||||
/// <summary>
|
||||
/// Incompatible with the user intent. Might even break something, we need to warn the user
|
||||
/// </summary>
|
||||
Incompatible,
|
||||
|
||||
/// <summary>
|
||||
/// Packages that are not visible for the User but useful for the analytics
|
||||
/// </summary>
|
||||
Hidden
|
||||
}
|
||||
|
||||
internal static class RecommendationTypeExtensions
|
||||
{
|
||||
public static bool IsRecommendedPackage(this RecommendationType type)
|
||||
=> type is RecommendationType.OptionalStandard or RecommendationType.NetcodeFeatured or RecommendationType.HostingFeatured;
|
||||
|
||||
public static bool IsRecommendedSolution(this RecommendationType type)
|
||||
=> type is RecommendationType.MainArchitectureChoice;
|
||||
|
||||
public static bool IsInstallableAsDirectDependency(this RecommendationType type)
|
||||
=> type is not RecommendationType.Incompatible and not RecommendationType.Hidden;
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b46a48f780f84026b9d1e73ee1b0121d
|
||||
timeCreated: 1702991052
|
@@ -0,0 +1,322 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Unity.Multiplayer.Center.Common;
|
||||
using Unity.Multiplayer.Center.Onboarding;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
internal static class RecommendationUtils
|
||||
{
|
||||
public static List<RecommendedPackageViewData> PackagesToInstall(RecommendationViewData recommendation,
|
||||
SolutionsToRecommendedPackageViewData solutionToPackageData)
|
||||
{
|
||||
var packagesToInstall = new List<RecommendedPackageViewData>();
|
||||
|
||||
// Can happen on first load
|
||||
if (recommendation?.NetcodeOptions == null)
|
||||
return packagesToInstall;
|
||||
|
||||
var selectedNetcode = GetSelectedNetcode(recommendation);
|
||||
if (selectedNetcode == null)
|
||||
return packagesToInstall;
|
||||
|
||||
// add features based on netcode
|
||||
if (selectedNetcode.MainPackage != null)
|
||||
packagesToInstall.Add(selectedNetcode.MainPackage);
|
||||
|
||||
var selectedServerArchitecture = GetSelectedHostingModel(recommendation);
|
||||
if (selectedServerArchitecture.MainPackage != null)
|
||||
packagesToInstall.Add(selectedServerArchitecture.MainPackage);
|
||||
|
||||
foreach (var package in solutionToPackageData.GetPackagesForSelection(selectedNetcode.Solution, selectedServerArchitecture.Solution))
|
||||
{
|
||||
if (package.Selected)
|
||||
{
|
||||
packagesToInstall.Add(package);
|
||||
}
|
||||
}
|
||||
|
||||
return packagesToInstall;
|
||||
}
|
||||
|
||||
public static RecommendedSolutionViewData GetSelectedHostingModel(RecommendationViewData recommendation)
|
||||
{
|
||||
return GetSelectedSolution(recommendation.ServerArchitectureOptions);
|
||||
}
|
||||
|
||||
public static RecommendedSolutionViewData GetSelectedNetcode(RecommendationViewData recommendation)
|
||||
{
|
||||
return GetSelectedSolution(recommendation.NetcodeOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first selected solution in the input array.
|
||||
/// </summary>
|
||||
/// <param name="availableSolutions">The available solutions.</param>
|
||||
/// <returns>Returns the first selected solution. If no solution is selected, it returns null.</returns>
|
||||
public static RecommendedSolutionViewData GetSelectedSolution(RecommendedSolutionViewData[] availableSolutions)
|
||||
{
|
||||
foreach (var solution in availableSolutions)
|
||||
{
|
||||
if (solution.Selected)
|
||||
{
|
||||
return solution;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static PackageDetails GetPackageDetailForPackageId(string packageId)
|
||||
{
|
||||
var idToPackageDetailDict = RecommenderSystemDataObject.instance.RecommenderSystemData.PackageDetailsById;
|
||||
|
||||
if (idToPackageDetailDict.TryGetValue(packageId, out var packageDetail))
|
||||
return packageDetail;
|
||||
|
||||
Debug.LogError("Trying to get package detail for package id that does not exist: " + packageId);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the packages passed via packageIds and their informal dependencies (stored in AdditionalPackages)
|
||||
/// </summary>
|
||||
/// <returns>List of PackageDetails</returns>
|
||||
/// <param name="packages">List of package id</param>
|
||||
/// <param name="toolTip">tooltip text</param>
|
||||
public static void GetPackagesWithAdditionalPackages(List<RecommendedPackageViewData> packages,
|
||||
out List<string> ids, out List<string> names, out string toolTip)
|
||||
{
|
||||
ids = new List<string>();
|
||||
names = new List<string>();
|
||||
var toolTipBuilder = new StringBuilder();
|
||||
foreach (var package in packages)
|
||||
{
|
||||
var packageDetail = GetPackageDetailForPackageId(package.PackageId);
|
||||
|
||||
var id = string.IsNullOrEmpty(package.PreReleaseVersion)? package.PackageId : $"{package.PackageId}@{package.PreReleaseVersion}";
|
||||
var name = string.IsNullOrEmpty(package.PreReleaseVersion)? packageDetail.Name : $"{packageDetail.Name} {package.PreReleaseVersion}";
|
||||
ids.Add(id);
|
||||
names.Add(name);
|
||||
toolTipBuilder.Append(name);
|
||||
|
||||
if (packageDetail.AdditionalPackages is {Length: > 0})
|
||||
{
|
||||
toolTipBuilder.Append(" + ");
|
||||
foreach (var additionalPackageId in packageDetail.AdditionalPackages)
|
||||
{
|
||||
var additionalPackage = GetPackageDetailForPackageId(additionalPackageId);
|
||||
ids.Add(additionalPackage.Id);
|
||||
names.Add(additionalPackage.Name);
|
||||
toolTipBuilder.Append(additionalPackage.Name);
|
||||
toolTipBuilder.Append(",");
|
||||
}
|
||||
}
|
||||
|
||||
toolTipBuilder.Append("\n");
|
||||
}
|
||||
|
||||
// remove last newline
|
||||
toolTip = toolTipBuilder.ToString().TrimEnd('\n');
|
||||
ids.Add(QuickstartIsMissingView.PackageId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reapplies the previous selection on the view data
|
||||
/// </summary>
|
||||
/// <param name="recommendation">The recommendation view data to update</param>
|
||||
/// <param name="data">The previous selection data</param>
|
||||
public static void ApplyPreviousSelection(RecommendationViewData recommendation, SelectedSolutionsData data)
|
||||
{
|
||||
if (data == null || recommendation == null)
|
||||
return;
|
||||
|
||||
if (data.SelectedNetcodeSolution != SelectedSolutionsData.NetcodeSolution.None)
|
||||
{
|
||||
foreach (var d in recommendation.NetcodeOptions)
|
||||
{
|
||||
d.Selected = Logic.ConvertNetcodeSolution(d) == data.SelectedNetcodeSolution;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.SelectedHostingModel != SelectedSolutionsData.HostingModel.None)
|
||||
{
|
||||
foreach (var view in recommendation.ServerArchitectureOptions)
|
||||
{
|
||||
view.Selected = Logic.ConvertInfrastructure(view) == data.SelectedHostingModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the packages that are of the given recommendation type
|
||||
/// </summary>
|
||||
/// <param name="packages">All the package view data as returned by the recommender system</param>
|
||||
/// <param name="type">The target recommendation type</param>
|
||||
/// <returns>The filtered list</returns>
|
||||
public static List<RecommendedPackageViewData> FilterByType(IEnumerable<RecommendedPackageViewData> packages, RecommendationType type)
|
||||
{
|
||||
var filteredPackages = new List<RecommendedPackageViewData>();
|
||||
|
||||
foreach (var package in packages)
|
||||
{
|
||||
if (package.RecommendationType == type)
|
||||
{
|
||||
filteredPackages.Add(package);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPackages;
|
||||
}
|
||||
|
||||
public static int IndexOfMaximumScore(RecommendedSolutionViewData[] array)
|
||||
{
|
||||
var maxIndex = -1;
|
||||
var maxScore = float.MinValue;
|
||||
for (var index = 0; index < array.Length; index++)
|
||||
{
|
||||
var solution = array[index];
|
||||
if (solution.RecommendationType == RecommendationType.Incompatible)
|
||||
continue;
|
||||
|
||||
if (solution.Score > maxScore)
|
||||
{
|
||||
maxScore = solution.Score;
|
||||
maxIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
return maxIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the recommended solution among the scored solutions.
|
||||
/// </summary>
|
||||
/// <param name="scoredSolutions">An array of tuples, where each tuple contains a PossibleSolution and its corresponding Scoring.</param>
|
||||
/// <returns>Returns the recommended solution with the maximum total score.</returns>
|
||||
public static PossibleSolution FindRecommendedSolution((PossibleSolution, Scoring)[] scoredSolutions)
|
||||
{
|
||||
var maxScore = float.MinValue;
|
||||
PossibleSolution recommendedSolution = default;
|
||||
foreach (var (possibleSolution, scoring) in scoredSolutions)
|
||||
{
|
||||
if (scoring.TotalScore > maxScore)
|
||||
{
|
||||
maxScore = scoring.TotalScore;
|
||||
recommendedSolution = possibleSolution;
|
||||
}
|
||||
}
|
||||
|
||||
return recommendedSolution;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks through the hosting models and marks them incompatible if necessary (deselected and recommendation
|
||||
/// type incompatible).
|
||||
/// Note that this might leave the recommendation in an invalid state (no hosting model selected)
|
||||
/// </summary>
|
||||
/// <param name="recommendation">The recommendation data to modify.</param>
|
||||
public static void MarkIncompatibleHostingModels(RecommendationViewData recommendation)
|
||||
{
|
||||
var netcode = GetSelectedNetcode(recommendation);
|
||||
|
||||
for (var index = 0; index < recommendation.ServerArchitectureOptions.Length; index++)
|
||||
{
|
||||
var hosting = recommendation.ServerArchitectureOptions[index];
|
||||
var isCompatible = RecommenderSystemDataObject.instance.RecommenderSystemData.IsHostingModelCompatibleWithNetcode(netcode.Solution, hosting.Solution, out _);
|
||||
if (!isCompatible)
|
||||
{
|
||||
hosting.RecommendationType = RecommendationType.Incompatible;
|
||||
hosting.Selected = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
hosting.RecommendationType = RecommendationType.SecondArchitectureChoice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a recommended package view by its ID from a list.
|
||||
/// </summary>
|
||||
/// <param name="packages">A list of Recommended Package ViewData representing the packages.</param>
|
||||
/// <param name="id">The ID of the package to find.</param>
|
||||
/// <returns>Returns the RecommendedPackageViewData object with the matching ID. If none is found, it returns null.</returns>
|
||||
public static RecommendedPackageViewData FindRecommendedPackageViewById( List<RecommendedPackageViewData> packages, string id)
|
||||
{
|
||||
RecommendedPackageViewData featureToSet = default;
|
||||
foreach (var package in packages)
|
||||
{
|
||||
if (package.PackageId == id)
|
||||
{
|
||||
featureToSet = package;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return featureToSet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specific question has been answered.
|
||||
/// </summary>
|
||||
/// <param name="question">The question to check.</param>
|
||||
/// <returns>Returns true if the question has been answered by the user, otherwise returns false.</returns>
|
||||
public static bool IsQuestionAnswered(Question question)
|
||||
{
|
||||
return Logic.TryGetAnswerByQuestionId(UserChoicesObject.instance.UserAnswers, question.Id, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two arrays of type <typeparamref name="T"/> for equality.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in the arrays.</typeparam>
|
||||
/// <param name="a">The first array to compare.</param>
|
||||
/// <param name="b">The second array to compare.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if all elements are equal and they are in the same order,
|
||||
/// <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
public static bool AreArraysEqual<T>(T[] a, T[] b)
|
||||
{
|
||||
if (a.Length != b.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < a.Length; i++)
|
||||
{
|
||||
if (!EqualityComparer<T>.Default.Equals(a[i], b[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the section type names in order from the provided section mapping.
|
||||
/// </summary>
|
||||
/// <param name="sectionMapping">A dictionary mapping OnboardingSectionCategory to SectionList types.</param>
|
||||
/// <returns>A list of section type names sorted in ascending order.</returns>
|
||||
public static List<string> GetSectionTypeNamesInOrder(Dictionary<OnboardingSectionCategory, Type[]> sectionMapping)
|
||||
{
|
||||
var sectionTypeNamesList = new List<string>();
|
||||
|
||||
foreach (var sectionTypeArray in sectionMapping.Values)
|
||||
{
|
||||
foreach (var sectionType in sectionTypeArray)
|
||||
{
|
||||
sectionTypeNamesList.Add(sectionType.AssemblyQualifiedName);
|
||||
}
|
||||
}
|
||||
|
||||
sectionTypeNamesList.Sort(StringComparer.InvariantCulture);
|
||||
return sectionTypeNamesList;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52a77c59214a4cb5a43fc7b0e097d832
|
||||
timeCreated: 1700470204
|
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains all the architectural options that we offer to users, with a score specific to their answers.
|
||||
/// This is the data that we show in the UI.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class RecommendationViewData
|
||||
{
|
||||
/// <summary>
|
||||
/// NGO or N4E or Other
|
||||
/// </summary>
|
||||
public RecommendedSolutionViewData[] NetcodeOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Client hosted / dedicated server.
|
||||
/// It comes with UGS services that we might or might not recommend
|
||||
/// </summary>
|
||||
public RecommendedSolutionViewData[] ServerArchitectureOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For each selection (netcode, hosting) model, there is a specific set of recommended packages
|
||||
/// This class stores this information and provides access to it.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class SolutionsToRecommendedPackageViewData
|
||||
{
|
||||
[SerializeField] RecommendedPackageViewDataArray[] m_Packages;
|
||||
public SolutionSelection[] Selections;
|
||||
|
||||
// Because we cannot serialize two-dimensional arrays
|
||||
[Serializable]
|
||||
internal struct RecommendedPackageViewDataArray
|
||||
{
|
||||
public RecommendedPackageViewData[] Packages;
|
||||
}
|
||||
|
||||
public SolutionsToRecommendedPackageViewData(SolutionSelection[] selections, RecommendedPackageViewData[][] packages)
|
||||
{
|
||||
Debug.Assert(selections.Length == packages.Length, "Selections and packages must have the same length");
|
||||
Selections = selections;
|
||||
m_Packages = new RecommendedPackageViewDataArray[packages.Length];
|
||||
for (var i = 0; i < packages.Length; i++)
|
||||
{
|
||||
m_Packages[i] = new RecommendedPackageViewDataArray {Packages = packages[i]};
|
||||
}
|
||||
}
|
||||
|
||||
public RecommendedPackageViewData[] GetPackagesForSelection(PossibleSolution netcode, PossibleSolution hosting)
|
||||
{
|
||||
return GetPackagesForSelection(new SolutionSelection(netcode, hosting));
|
||||
}
|
||||
|
||||
public RecommendedPackageViewData[] GetPackagesForSelection(SolutionSelection selection)
|
||||
{
|
||||
var index = Array.IndexOf(Selections, selection);
|
||||
return index < 0 ? Array.Empty<RecommendedPackageViewData>() : m_Packages[index].Packages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base fields for things that we can recommend (typically a package or a solution).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class RecommendedItemViewData
|
||||
{
|
||||
/// <summary>
|
||||
/// How much we want to recommend this item. With the score, this is what is modified by the recommender system.
|
||||
/// The architecture option with the best score will be marked as MainArchitectureChoice, the other as SecondArchitectureChoice,
|
||||
/// which enables us to highlight the recommended option.
|
||||
/// </summary>
|
||||
public RecommendationType RecommendationType;
|
||||
|
||||
/// <summary>
|
||||
/// Item is part of the selection, because it was preselected or user selected it.
|
||||
/// </summary>
|
||||
public bool Selected;
|
||||
|
||||
/// <summary>
|
||||
/// Optional: reason why this recommendation was made
|
||||
/// </summary>
|
||||
public string Reason;
|
||||
|
||||
/// <summary>
|
||||
/// Url to feature documentation
|
||||
/// </summary>
|
||||
public string DocsUrl;
|
||||
|
||||
/// <summary>
|
||||
/// Recommendation is installed and direct dependency
|
||||
/// </summary>
|
||||
public bool IsInstalledAsProjectDependency;
|
||||
|
||||
/// <summary>
|
||||
/// Installed version number of a package
|
||||
/// </summary>
|
||||
public string InstalledVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Architectural solution (netcode or server architecture) that we offer. It comes with an optional main package and
|
||||
/// associated recommended packages.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class RecommendedSolutionViewData : RecommendedItemViewData
|
||||
{
|
||||
public string Title;
|
||||
|
||||
public PossibleSolution Solution;
|
||||
|
||||
/// <summary>
|
||||
/// How much of a match is this item. Computed by recommender system based on answers.
|
||||
/// </summary>
|
||||
public float Score;
|
||||
|
||||
/// <summary>
|
||||
/// The main package to install for this solution (note that this might be null, e.g. for client hosted game)
|
||||
/// </summary>
|
||||
public RecommendedPackageViewData MainPackage;
|
||||
|
||||
public string WarningString;
|
||||
|
||||
public RecommendedSolutionViewData(RecommenderSystemData data, RecommendedSolution solution,
|
||||
RecommendationType type, Scoring scoring, Dictionary<string, string> installedPackageDictionary)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(solution.MainPackageId))
|
||||
{
|
||||
var mainPackageDetails = data.PackageDetailsById[solution.MainPackageId];
|
||||
DocsUrl = string.IsNullOrEmpty(solution.DocUrl) ? mainPackageDetails.DocsUrl : solution.DocUrl;
|
||||
|
||||
if (installedPackageDictionary.ContainsKey(solution.MainPackageId))
|
||||
{
|
||||
InstalledVersion = installedPackageDictionary[solution.MainPackageId];
|
||||
IsInstalledAsProjectDependency = PackageManagement.IsDirectDependency(solution.MainPackageId);
|
||||
}
|
||||
|
||||
MainPackage = new RecommendedPackageViewData(mainPackageDetails, type, InstalledVersion);
|
||||
}
|
||||
else
|
||||
{
|
||||
DocsUrl = solution.DocUrl;
|
||||
}
|
||||
|
||||
RecommendationType = type;
|
||||
Title = solution.Title;
|
||||
Reason = scoring?.GetReasonString();
|
||||
Score = scoring?.TotalScore ?? 0f;
|
||||
Selected = RecommendationType.IsRecommendedSolution();
|
||||
|
||||
Solution = solution.Type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single package that is part of a recommendation.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class RecommendedPackageViewData : RecommendedItemViewData
|
||||
{
|
||||
public string PackageId;
|
||||
|
||||
public string Name;
|
||||
|
||||
public string PreReleaseVersion;
|
||||
|
||||
/// <summary>
|
||||
/// A short description of the feature.
|
||||
/// </summary>
|
||||
public string ShortDescription = "Short description not added yet";
|
||||
|
||||
public RecommendedPackageViewData(PackageDetails details, RecommendationType type, string installedVersion=null, string reason = null)
|
||||
{
|
||||
RecommendationType = type;
|
||||
PackageId = details.Id;
|
||||
Name = details.Name;
|
||||
PreReleaseVersion = details.PreReleaseVersion;
|
||||
Selected = type.IsRecommendedPackage();
|
||||
ShortDescription = details.ShortDescription;
|
||||
Reason = reason;
|
||||
DocsUrl = details.DocsUrl;
|
||||
InstalledVersion = installedVersion;
|
||||
IsInstalledAsProjectDependency = installedVersion != null && PackageManagement.IsDirectDependency(PackageId);
|
||||
}
|
||||
|
||||
public RecommendedPackageViewData(PackageDetails details, RecommendedPackage recommendation, string installedVersion=null)
|
||||
: this(details, recommendation.Type, installedVersion, recommendation.Reason)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ddc3b8a12c84454b04e4d6bdd88241c
|
||||
timeCreated: 1695734131
|
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Multiplayer.Center.Common;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
using AnswerWithQuestion = Tuple<Question, Answer>;
|
||||
|
||||
/// <summary>
|
||||
/// Builds recommendation views based on Questionnaire data and Answer data.
|
||||
/// The recommendation is based on the scoring of the answers, which is controlled by the RecommenderSystemData.
|
||||
/// </summary>
|
||||
internal static class RecommenderSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Main entry point for the recommender system: computes the recommendation based on the questionnaire data and
|
||||
/// the answers.
|
||||
/// If no answer has been given or the questionnaire does not match the answers, this returns null.
|
||||
/// </summary>
|
||||
/// <param name="questionnaireData">The questionnaire that the user filled.</param>
|
||||
/// <param name="answerData">The answers the user gave.</param>
|
||||
/// <returns>The recommendation view data.</returns>
|
||||
public static RecommendationViewData GetRecommendation(QuestionnaireData questionnaireData, AnswerData answerData)
|
||||
{
|
||||
var answers = CollectAnswers(questionnaireData, answerData);
|
||||
|
||||
// Note: valid now only because we do not have multiple answers per question
|
||||
if (answers.Count < questionnaireData.Questions.Length) return null;
|
||||
|
||||
var data = RecommenderSystemDataObject.instance.RecommenderSystemData;
|
||||
var scoredSolutions = CalculateScore(data, answers);
|
||||
|
||||
return CreateRecommendation(data, scoredSolutions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the view data for all possible solution selections.
|
||||
/// </summary>
|
||||
/// <returns>The constructed set of views</returns>
|
||||
public static SolutionsToRecommendedPackageViewData GetSolutionsToRecommendedPackageViewData()
|
||||
{
|
||||
var data = RecommenderSystemDataObject.instance.RecommenderSystemData;
|
||||
var installedPackageDictionary = PackageManagement.InstalledPackageDictionary();
|
||||
var selections = new SolutionSelection[16];
|
||||
var packages = new RecommendedPackageViewData[16][];
|
||||
PossibleSolution[] netcodes = { PossibleSolution.NGO, PossibleSolution.N4E, PossibleSolution.CustomNetcode, PossibleSolution.NoNetcode };
|
||||
PossibleSolution[] hostings = { PossibleSolution.LS, PossibleSolution.DS, PossibleSolution.CloudCode, PossibleSolution.DA };
|
||||
|
||||
var index = 0;
|
||||
foreach (var netcode in netcodes)
|
||||
{
|
||||
foreach (var hosting in hostings)
|
||||
{
|
||||
var selection = new SolutionSelection(netcode, hosting);
|
||||
selections[index] = selection;
|
||||
packages[index] = BuildRecommendationForSelection(data, selection, installedPackageDictionary);
|
||||
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
return new SolutionsToRecommendedPackageViewData(selections, packages);
|
||||
}
|
||||
|
||||
public static void AdaptRecommendationToNetcodeSelection(RecommendationViewData recommendation)
|
||||
{
|
||||
RecommendationUtils.MarkIncompatibleHostingModels(recommendation);
|
||||
var maxIndex = RecommendationUtils.IndexOfMaximumScore(recommendation.ServerArchitectureOptions);
|
||||
recommendation.ServerArchitectureOptions[maxIndex].RecommendationType = RecommendationType.MainArchitectureChoice;
|
||||
if (RecommendationUtils.GetSelectedHostingModel(recommendation) == null)
|
||||
recommendation.ServerArchitectureOptions[maxIndex].Selected = true;
|
||||
}
|
||||
|
||||
static List<AnswerWithQuestion> CollectAnswers(QuestionnaireData questionnaireData, AnswerData answerData)
|
||||
{
|
||||
if (questionnaireData?.Questions == null || questionnaireData.Questions.Length == 0)
|
||||
throw new ArgumentException("Questionnaire data is null or empty", nameof(questionnaireData));
|
||||
|
||||
List<AnswerWithQuestion> givenAnswers = new();
|
||||
|
||||
var answers = answerData.Answers;
|
||||
|
||||
foreach (var answeredQuestion in answers)
|
||||
{
|
||||
// find question for the answer
|
||||
if (!Logic.TryGetQuestionByQuestionId(questionnaireData, answeredQuestion.QuestionId, out var question))
|
||||
continue;
|
||||
|
||||
// find answer object for the given answer id
|
||||
foreach (var answerId in answeredQuestion.Answers)
|
||||
{
|
||||
if (!Logic.TryGetAnswerByAnswerId(question, answerId, out var choice))
|
||||
continue;
|
||||
givenAnswers.Add(Tuple.Create(question, choice));
|
||||
}
|
||||
}
|
||||
|
||||
return givenAnswers;
|
||||
}
|
||||
|
||||
static Dictionary<PossibleSolution, Scoring> CalculateScore(RecommenderSystemData data, List<AnswerWithQuestion> answers)
|
||||
{
|
||||
var possibleSolutions = Enum.GetValues(typeof(PossibleSolution));
|
||||
Dictionary<PossibleSolution, Scoring> scores = new(possibleSolutions.Length);
|
||||
|
||||
foreach (var solution in possibleSolutions)
|
||||
{
|
||||
var solutionObject = data.SolutionsByType[(PossibleSolution) solution];
|
||||
scores.Add((PossibleSolution) solution, new Scoring(solutionObject.ShortDescription));
|
||||
}
|
||||
|
||||
foreach (var (question, answer) in answers)
|
||||
{
|
||||
foreach (var scoreImpact in answer.ScoreImpacts)
|
||||
{
|
||||
scores[scoreImpact.Solution].AddScore(scoreImpact.Score * question.GlobalWeight, scoreImpact.Comment);
|
||||
}
|
||||
}
|
||||
|
||||
return scores;
|
||||
}
|
||||
|
||||
static RecommendationViewData CreateRecommendation(RecommenderSystemData data, IReadOnlyDictionary<PossibleSolution, Scoring> scoredSolutions)
|
||||
{
|
||||
RecommendationViewData recommendation = new();
|
||||
var installedPackageDictionary = PackageManagement.InstalledPackageDictionary();
|
||||
|
||||
recommendation.NetcodeOptions = BuildRecommendedSolutions(data, new [] {
|
||||
(PossibleSolution.NGO, scoredSolutions[PossibleSolution.NGO]),
|
||||
(PossibleSolution.N4E, scoredSolutions[PossibleSolution.N4E]),
|
||||
(PossibleSolution.CustomNetcode, scoredSolutions[PossibleSolution.CustomNetcode]),
|
||||
(PossibleSolution.NoNetcode, scoredSolutions[PossibleSolution.NoNetcode]) },
|
||||
installedPackageDictionary);
|
||||
|
||||
recommendation.ServerArchitectureOptions = BuildRecommendedSolutions(data, new [] {
|
||||
(PossibleSolution.LS, scoredSolutions[PossibleSolution.LS]),
|
||||
(PossibleSolution.DS, scoredSolutions[PossibleSolution.DS]),
|
||||
(PossibleSolution.CloudCode, scoredSolutions[PossibleSolution.CloudCode]),
|
||||
(PossibleSolution.DA, scoredSolutions[PossibleSolution.DA]) },
|
||||
installedPackageDictionary);
|
||||
|
||||
AdaptRecommendationToNetcodeSelection(recommendation);
|
||||
return recommendation;
|
||||
}
|
||||
|
||||
static RecommendedSolutionViewData[] BuildRecommendedSolutions(RecommenderSystemData data, (PossibleSolution, Scoring)[] scoredSolutions, Dictionary<string, string> installedPackageDictionary)
|
||||
{
|
||||
var recommendedSolution = RecommendationUtils.FindRecommendedSolution(scoredSolutions);
|
||||
var result = new RecommendedSolutionViewData[scoredSolutions.Length];
|
||||
|
||||
for (var index = 0; index < scoredSolutions.Length; index++)
|
||||
{
|
||||
var scoredSolution = scoredSolutions[index];
|
||||
var recoType = scoredSolution.Item1 == recommendedSolution ? RecommendationType.MainArchitectureChoice : RecommendationType.SecondArchitectureChoice;
|
||||
var reco = new RecommendedSolutionViewData(data, data.SolutionsByType[scoredSolution.Item1], recoType, scoredSolution.Item2, installedPackageDictionary);
|
||||
result[index] = reco;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static RecommendedPackageViewData[] BuildRecommendationForSelection(RecommenderSystemData data, SolutionSelection selection, Dictionary<string, string> installedPackageDictionary)
|
||||
{
|
||||
// Note: working on a copy that we modify
|
||||
var netcodePackages = (RecommendedPackage[]) data.SolutionsByType[selection.Netcode].RecommendedPackages.Clone();
|
||||
var hostingOverrides = data.SolutionsByType[selection.HostingModel].RecommendedPackages;
|
||||
foreach (var package in hostingOverrides)
|
||||
{
|
||||
var existing = Array.FindIndex(netcodePackages, p => p.PackageId == package.PackageId);
|
||||
if (existing == -1)
|
||||
{
|
||||
Debug.LogError($"Malformed data for hosting model {selection.HostingModel}: package {package.PackageId} not found in netcode packages of {selection.Netcode}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
netcodePackages[existing] = package;
|
||||
}
|
||||
|
||||
var result = new RecommendedPackageViewData[netcodePackages.Length];
|
||||
for (var index = 0; index < netcodePackages.Length; index++)
|
||||
{
|
||||
var package = netcodePackages[index];
|
||||
installedPackageDictionary.TryGetValue(package.PackageId, out var installedVersion);
|
||||
result[index] = new RecommendedPackageViewData( data.PackageDetailsById[package.PackageId], package, installedVersion);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1c995459c0d418c88dd274e04840c5d
|
||||
timeCreated: 1695733765
|
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Multiplayer.Center.Questionnaire;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
/// <summary>
|
||||
/// The source of data for the recommender system. Based on scoring and this data, the recommender system will
|
||||
/// populate the recommendation view data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class RecommenderSystemData
|
||||
{
|
||||
/// <summary>
|
||||
/// The Unity version for which this recommendation data is valid.
|
||||
/// </summary>
|
||||
public string TargetUnityVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Stores all the recommended solutions. This is serialized.
|
||||
/// </summary>
|
||||
public RecommendedSolution[] RecommendedSolutions;
|
||||
|
||||
/// <summary>
|
||||
/// Stores all the package details.
|
||||
/// This is serialized.
|
||||
/// </summary>
|
||||
public PackageDetails[] Packages;
|
||||
|
||||
/// <summary>
|
||||
/// Provides convenient access to the package details by package id.
|
||||
/// </summary>
|
||||
public Dictionary<string, PackageDetails> PackageDetailsById
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_PackageDetailsById != null) return m_PackageDetailsById;
|
||||
m_PackageDetailsById = Utils.ToDictionary(Packages, p => p.Id);
|
||||
return m_PackageDetailsById;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<PossibleSolution, RecommendedSolution> SolutionsByType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_SolutionsByType != null) return m_SolutionsByType;
|
||||
m_SolutionsByType = Utils.ToDictionary(RecommendedSolutions, s => s.Type);
|
||||
return m_SolutionsByType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for incompatibility between the netcode and hosting model.
|
||||
/// </summary>
|
||||
/// <param name="netcode">The netcode type</param>
|
||||
/// <param name="hostingModel">The hosting model</param>
|
||||
/// <param name="reason">The reason for the incompatibility, filled when this function returns false.</param>
|
||||
/// <returns>True if compatible (default), False otherwise</returns>
|
||||
public bool IsHostingModelCompatibleWithNetcode(PossibleSolution netcode, PossibleSolution hostingModel, out string reason)
|
||||
{
|
||||
m_IncompatibleHostingModels ??= Utils.ToDictionary(RecommendedSolutions);
|
||||
return !m_IncompatibleHostingModels.TryGetValue(new SolutionSelection(netcode, hostingModel), out reason);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patch incompatibility values.
|
||||
/// </summary>
|
||||
/// <param name="netcode">Netcode solution</param>
|
||||
/// <param name="hostingModel">Hosting model solution</param>
|
||||
/// <param name="newIsCompatible">Whether we should now consider the two solutions compatible</param>
|
||||
/// <param name="reason">If incompatible, why it is incompatible.</param>
|
||||
internal void UpdateIncompatibility(PossibleSolution netcode, PossibleSolution hostingModel, bool newIsCompatible, string reason=null)
|
||||
{
|
||||
Utils.UpdateIncompatibilityInSolutions(RecommendedSolutions, netcode, hostingModel, newIsCompatible, reason);
|
||||
m_IncompatibleHostingModels = Utils.ToDictionary(RecommendedSolutions);
|
||||
m_SolutionsByType = Utils.ToDictionary(RecommendedSolutions, s => s.Type);
|
||||
}
|
||||
|
||||
Dictionary<string, PackageDetails> m_PackageDetailsById;
|
||||
Dictionary<PossibleSolution, RecommendedSolution> m_SolutionsByType;
|
||||
Dictionary<SolutionSelection, string> m_IncompatibleHostingModels;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal struct SolutionSelection
|
||||
{
|
||||
public PossibleSolution Netcode;
|
||||
public PossibleSolution HostingModel;
|
||||
public SolutionSelection(PossibleSolution netcode, PossibleSolution hostingModel)
|
||||
{
|
||||
Netcode = netcode;
|
||||
HostingModel = hostingModel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A possible solution and whether packages are recommended or not
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class RecommendedSolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of solution
|
||||
/// </summary>
|
||||
public PossibleSolution Type;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the solution as shown in the UI.
|
||||
/// </summary>
|
||||
public string Title;
|
||||
|
||||
/// <summary>
|
||||
/// Optional package ID associated with that solution (e.g. a netcode package or the cloud code package).
|
||||
/// Use this field if the package has to mandatorily be installed when the solution is selected.
|
||||
/// </summary>
|
||||
public string MainPackageId;// only id because scoring will impact the rest
|
||||
|
||||
/// <summary>
|
||||
/// Url to documentation describing the solution.
|
||||
/// </summary>
|
||||
public string DocUrl;
|
||||
|
||||
/// <summary>
|
||||
/// Short description of the solution.
|
||||
/// </summary>
|
||||
public string ShortDescription;
|
||||
|
||||
/// <summary>
|
||||
/// The packages and the associated recommendation type.
|
||||
/// If the Type is a netcode Type, all the possible packages should be in this array.
|
||||
/// If the Type is a hosting model, this will contain only overrides in case a package is incompatible or
|
||||
/// featured for the hosting model.
|
||||
/// </summary>
|
||||
public RecommendedPackage[] RecommendedPackages;
|
||||
|
||||
/// <summary>
|
||||
/// Solutions that are incompatible with this solution.
|
||||
/// Typically used for netcode solutions.
|
||||
/// </summary>
|
||||
public IncompatibleSolution[] IncompatibleSolutions = Array.Empty<IncompatibleSolution>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores why a solution is incompatible with something and why.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class IncompatibleSolution
|
||||
{
|
||||
/// <summary>
|
||||
/// What is incompatible.
|
||||
/// </summary>
|
||||
public PossibleSolution Solution;
|
||||
|
||||
/// <summary>
|
||||
/// Why it is incompatible.
|
||||
/// </summary>
|
||||
public string Reason;
|
||||
|
||||
public IncompatibleSolution(PossibleSolution solution, string reason)
|
||||
{
|
||||
Solution = solution;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A package, whether it is recommended or not (context dependent), and why.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class RecommendedPackage
|
||||
{
|
||||
/// <summary>
|
||||
/// The package id (e.g. com.unity.netcode)
|
||||
/// </summary>
|
||||
public string PackageId;
|
||||
|
||||
/// <summary>
|
||||
/// Whether it is recommended or not.
|
||||
/// </summary>
|
||||
public RecommendationType Type;
|
||||
|
||||
/// <summary>
|
||||
/// Why it is recommended or not.
|
||||
/// </summary>
|
||||
public string Reason;
|
||||
|
||||
public RecommendedPackage(string packageId, RecommendationType type, string reason)
|
||||
{
|
||||
PackageId = packageId;
|
||||
Type = type;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal class PackageDetails
|
||||
{
|
||||
public string Id;
|
||||
public string Name;
|
||||
public string ShortDescription;
|
||||
public string DocsUrl;
|
||||
public string[] AdditionalPackages;
|
||||
|
||||
/// <summary>
|
||||
/// In case we want to promote a specific pre-release version, this is set (by default, this is null
|
||||
/// and the default package manager version is used).
|
||||
/// </summary>
|
||||
public string PreReleaseVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Details about the package.
|
||||
/// </summary>
|
||||
/// <param name="id">Package ID</param>
|
||||
/// <param name="name">Package Name (for display)</param>
|
||||
/// <param name="shortDescription">Short description.</param>
|
||||
/// <param name="docsUrl">Link to Docs</param>
|
||||
/// <param name="additionalPackages">Ids of packages that should be installed along this one, but are not formal dependencies.</param>
|
||||
public PackageDetails(string id, string name, string shortDescription, string docsUrl, string[] additionalPackages = null)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
ShortDescription = shortDescription;
|
||||
DocsUrl = docsUrl;
|
||||
AdditionalPackages = additionalPackages;
|
||||
}
|
||||
}
|
||||
|
||||
static class Utils
|
||||
{
|
||||
public static Dictionary<TKey, T> ToDictionary<T, TKey>(T[] array, Func<T, TKey> keySelector)
|
||||
{
|
||||
if (array == null) return null;
|
||||
var result = new Dictionary<TKey, T>();
|
||||
foreach (var item in array)
|
||||
{
|
||||
result[keySelector(item)] = item;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Dictionary<SolutionSelection, string> ToDictionary(RecommendedSolution[] solutions)
|
||||
{
|
||||
var result = new Dictionary<SolutionSelection, string>();
|
||||
foreach (var recommendedSolution in solutions)
|
||||
{
|
||||
foreach (var incompatibleHostingModel in recommendedSolution.IncompatibleSolutions)
|
||||
{
|
||||
var key = new SolutionSelection(recommendedSolution.Type, incompatibleHostingModel.Solution);
|
||||
result.Add(key, incompatibleHostingModel.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void UpdateIncompatibilityInSolutions(RecommendedSolution[] solutions, PossibleSolution netcode,
|
||||
PossibleSolution hostingModel, bool newIsCompatible, string reason)
|
||||
{
|
||||
foreach (var recommendedSolution in solutions)
|
||||
{
|
||||
if (recommendedSolution.Type != netcode) continue;
|
||||
|
||||
var incompatibleSolution = Array.Find(recommendedSolution.IncompatibleSolutions, s => s.Solution == hostingModel);
|
||||
if (newIsCompatible && incompatibleSolution != null)
|
||||
{
|
||||
recommendedSolution.IncompatibleSolutions = Array.FindAll(recommendedSolution.IncompatibleSolutions, s => s.Solution != hostingModel);
|
||||
}
|
||||
else if (!newIsCompatible && incompatibleSolution == null)
|
||||
{
|
||||
Array.Resize(ref recommendedSolution.IncompatibleSolutions, recommendedSolution.IncompatibleSolutions.Length + 1);
|
||||
recommendedSolution.IncompatibleSolutions[^1] = new IncompatibleSolution(hostingModel, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38a11839c22b4e67aa2e508d906edf65
|
||||
timeCreated: 1702913830
|
@@ -0,0 +1,35 @@
|
||||
using UnityEditor;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
/// <summary>
|
||||
/// Current way to fetch recommendation data from disk. Will probably change to fetching something from a server.
|
||||
/// </summary>
|
||||
[FilePath(PathConstants.RecommendationDataPath, FilePathAttribute.Location.ProjectFolder)]
|
||||
internal class RecommenderSystemDataObject : ScriptableSingleton<RecommenderSystemDataObject>
|
||||
{
|
||||
public RecommenderSystemData RecommenderSystemData;
|
||||
|
||||
#if MULTIPLAYER_CENTER_DEV_MODE
|
||||
[MenuItem("Multiplayer/Recommendations/Populate Default Recommendation Data")]
|
||||
public static void CreateDefaultInstance()
|
||||
{
|
||||
instance.RecommenderSystemData = RecommendationAssetUtils.PopulateDefaultRecommendationData();
|
||||
instance.ForceSave();
|
||||
}
|
||||
|
||||
void ForceSave()
|
||||
{
|
||||
base.Save(saveAsText:true);
|
||||
AssetDatabase.Refresh();
|
||||
DestroyImmediate(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static class PathConstants
|
||||
{
|
||||
const string k_RootPath = "Packages/com.unity.multiplayer.center/Editor/Recommendations/";
|
||||
public const string RecommendationDataPath = k_RootPath + "RecommendationData_6000.0.recommendations";
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6a02e450cd04bb781e7728f2056251c
|
||||
timeCreated: 1702913905
|
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Multiplayer.Center.Recommendations
|
||||
{
|
||||
struct ScoreWithReason
|
||||
{
|
||||
public float Score;
|
||||
public string Reason;
|
||||
|
||||
public ScoreWithReason(float score, string reason)
|
||||
{
|
||||
Score = score;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates scores for a given solution and fetches the reasons for the highest score
|
||||
/// </summary>
|
||||
internal class Scoring
|
||||
{
|
||||
public const string DynamicKeyword = "[dynamic]";
|
||||
|
||||
string m_PrimaryDescription = null;
|
||||
|
||||
List<ScoreWithReason> m_AllScores = new();
|
||||
public float TotalScore { get; private set; } = 0f;
|
||||
|
||||
public Scoring(string primaryDescription)
|
||||
{
|
||||
m_PrimaryDescription = primaryDescription;
|
||||
}
|
||||
|
||||
public void AddScore(float score, string reason)
|
||||
{
|
||||
TotalScore += score;
|
||||
if(m_AllScores.Count == 0)
|
||||
{
|
||||
m_AllScores.Add(new ScoreWithReason(score, reason));
|
||||
return;
|
||||
}
|
||||
|
||||
int insertIndex = 0;
|
||||
for (int i = 0; i < m_AllScores.Count; i++)
|
||||
{
|
||||
if (score > m_AllScores[i].Score)
|
||||
{
|
||||
insertIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_AllScores.Insert(insertIndex, new ScoreWithReason(score, reason));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reason for increased scores
|
||||
/// </summary>
|
||||
/// <returns>The explanatory string</returns>
|
||||
public string GetReasonString() => GetAllContributionsReasons();
|
||||
|
||||
string GetAllContributionsReasons()
|
||||
{
|
||||
return string.IsNullOrEmpty(m_PrimaryDescription)? OneReasonPerLine() : CombineReasonsInOneSentence();
|
||||
}
|
||||
|
||||
string OneReasonPerLine()
|
||||
{
|
||||
var stringBuilder = new System.Text.StringBuilder();
|
||||
foreach (var score in m_AllScores)
|
||||
{
|
||||
stringBuilder.AppendLine(score.Reason);
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
string CombineReasonsInOneSentence()
|
||||
{
|
||||
var length = m_AllScores.Count;
|
||||
if(length == 0)
|
||||
return RemoveSentenceWithDynamicKeyword(m_PrimaryDescription);
|
||||
|
||||
var explanation = length switch
|
||||
{
|
||||
1 => m_AllScores[0].Reason,
|
||||
2 => $"{m_AllScores[0].Reason} and {m_AllScores[1].Reason}",
|
||||
_ => Combine(m_AllScores)
|
||||
};
|
||||
|
||||
return m_PrimaryDescription.Replace(DynamicKeyword, explanation);
|
||||
}
|
||||
|
||||
static string RemoveSentenceWithDynamicKeyword(string primaryDescription)
|
||||
{
|
||||
var sentences = primaryDescription.Split('.');
|
||||
var sentencesWithoutKeyword = new StringBuilder(capacity:sentences.Length);
|
||||
var index = 0;
|
||||
foreach (var sentence in sentences)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sentence) && !sentence.Contains(DynamicKeyword))
|
||||
{
|
||||
sentencesWithoutKeyword.Append(sentence);
|
||||
sentencesWithoutKeyword.Append(".");
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return sentencesWithoutKeyword.ToString();
|
||||
}
|
||||
|
||||
static string Combine(List<ScoreWithReason> scores)
|
||||
{
|
||||
var stringBuilder = new System.Text.StringBuilder();
|
||||
for(var i = 0; i < scores.Count -2; i++)
|
||||
{
|
||||
stringBuilder.Append(scores[i].Reason);
|
||||
stringBuilder.Append(", ");
|
||||
}
|
||||
|
||||
stringBuilder.Append(scores[^2].Reason);
|
||||
stringBuilder.Append(" and ");
|
||||
stringBuilder.Append(scores[^1].Reason);
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f526d484b0c04a1896c469f31c713786
|
||||
timeCreated: 1699892140
|
Reference in New Issue
Block a user