From c43b7ab1419e40186465f43439ef61aa4e591421 Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Thu, 11 Dec 2025 10:27:01 -0300 Subject: [PATCH 1/9] new: Introduced BeamSemanticTypeAttribute and IBeamSemanticType.cs (with a few implementations) change: Updated OpenAPI gen to use it correctly and add the extensions needed for each case. --- .../Runtime/BeamSemanticTypeAttribute.cs | 33 +++++ .../Implementations/ServiceConstants.cs | 5 + .../Runtime/Semantics/BeamAccountId.cs | 62 +++++++++ .../Runtime/Semantics/BeamCid.cs | 60 ++++++++ .../Runtime/Semantics/BeamContentId.cs | 30 ++++ .../Semantics/BeamContentManifestId.cs | 25 ++++ .../Runtime/Semantics/BeamGamerTag.cs | 61 +++++++++ .../Runtime/Semantics/BeamPid.cs | 25 ++++ .../Runtime/Semantics/BeamStats.cs | 35 +++++ .../Runtime/Semantics/IBeamSemanticType.cs | 6 + .../JsonConverters/BeamAccountIdConverter.cs | 36 +++++ .../JsonConverters/BeamCidConverter.cs | 33 +++++ .../JsonConverters/BeamGamerTagConverter.cs | 33 +++++ .../Runtime/Semantics/ServiceName.cs | 7 +- cli/cli/Contants.cs | 2 +- .../Editor/BeamCli/Commands/Beam.cs | 17 +-- .../Editor/BeamCli/Commands/BeamInit.cs | 4 +- .../Editor/BeamCli/Commands/BeamLogin.cs | 2 +- .../Commands/BeamProjectGenerateProperties.cs | 128 +++++++++--------- .../BeamCli/Commands/BeamProjectNewService.cs | 2 +- .../BeamCli/Commands/BeamProjectNewStorage.cs | 2 +- .../OpenAPI/SchemaGenerator.cs | 26 +++- .../OpenAPI/ServiceDocGenerator.cs | 30 +++- .../UnityJsonContractResolver.cs | 4 + .../OpenAPITests/TypeTests.cs | 16 +++ 25 files changed, 592 insertions(+), 92 deletions(-) create mode 100644 cli/beamable.common/Runtime/BeamSemanticTypeAttribute.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamAccountId.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamCid.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamContentId.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamPid.cs create mode 100644 cli/beamable.common/Runtime/Semantics/BeamStats.cs create mode 100644 cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs create mode 100644 cli/beamable.common/Runtime/Semantics/JsonConverters/BeamAccountIdConverter.cs create mode 100644 cli/beamable.common/Runtime/Semantics/JsonConverters/BeamCidConverter.cs create mode 100644 cli/beamable.common/Runtime/Semantics/JsonConverters/BeamGamerTagConverter.cs diff --git a/cli/beamable.common/Runtime/BeamSemanticTypeAttribute.cs b/cli/beamable.common/Runtime/BeamSemanticTypeAttribute.cs new file mode 100644 index 0000000000..305fa4109c --- /dev/null +++ b/cli/beamable.common/Runtime/BeamSemanticTypeAttribute.cs @@ -0,0 +1,33 @@ +using System; + +namespace Beamable +{ + + public enum BeamSemanticType + { + Cid, + Pid, + AccountId, + GamerTag, + ContentManifestId, + ContentId, + StatsType, + ServiceName, + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Class | AttributeTargets.Struct)] + public class BeamSemanticTypeAttribute : Attribute + { + public string SemanticType { get; } + + public BeamSemanticTypeAttribute(BeamSemanticType semanticType) + { + SemanticType = semanticType.ToString(); + } + + public BeamSemanticTypeAttribute(string customSemanticType) + { + SemanticType = customSemanticType; + } + } +} diff --git a/cli/beamable.common/Runtime/Constants/Implementations/ServiceConstants.cs b/cli/beamable.common/Runtime/Constants/Implementations/ServiceConstants.cs index c0abb4a5cf..bb4894144f 100644 --- a/cli/beamable.common/Runtime/Constants/Implementations/ServiceConstants.cs +++ b/cli/beamable.common/Runtime/Constants/Implementations/ServiceConstants.cs @@ -79,6 +79,11 @@ public static partial class Services public const string PATH_CALLABLE_METHOD_NAME_KEY = "x-beamable-callable-method-name"; public const string PATH_CALLABLE_METHOD_CLIENT_PREFIX_KEY = "x-beamable-route-source-client-prefix"; + /// + /// OpenAPI extension that describes the semantic type of a primitive field. + /// + public const string SCHEMA_SEMANTIC_TYPE_NAME_KEY = "x-beamable-semantic-type"; + public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_INTERFACE_KEY = "interface"; public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_ID_KEY = "federationId"; public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_CLASS_NAME_KEY = "federationClassName"; diff --git a/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs b/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs new file mode 100644 index 0000000000..39b78dd89f --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs @@ -0,0 +1,62 @@ +using Beamable.Common.BeamCli; +using System; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.AccountId)] + public struct BeamAccountId : IBeamSemanticType + { + private long _longValue; + private string _stringValue; + + public long AsLong + { + get => _longValue; + set + { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamAccountId(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamAccountId(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamAccountId id) => id.AsString; + public static implicit operator long(BeamAccountId id) => id.AsLong; + + public static implicit operator BeamAccountId(string value) => new BeamAccountId(value); + public static implicit operator BeamAccountId(long value) => new BeamAccountId(value); + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamCid.cs b/cli/beamable.common/Runtime/Semantics/BeamCid.cs new file mode 100644 index 0000000000..a04858f81c --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamCid.cs @@ -0,0 +1,60 @@ +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.Cid)] + public struct BeamCid : IBeamSemanticType + { + private long _longValue; + private string _stringValue; + + public long AsLong { + get => _longValue; + set { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamCid(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamCid(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamCid cid) => cid.AsString; + public static implicit operator long(BeamCid cid) => cid.AsLong; + + public static implicit operator BeamCid(string value) => new BeamCid(value); + public static implicit operator BeamCid(long value) => new BeamCid(value); + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamContentId.cs b/cli/beamable.common/Runtime/Semantics/BeamContentId.cs new file mode 100644 index 0000000000..09ca8a37d0 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamContentId.cs @@ -0,0 +1,30 @@ +using System; +using Beamable.Common.BeamCli; +using Beamable.Common.Content; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.ContentId)] + public struct BeamContentId : IBeamSemanticType + { + private string _value; + + public string AsString + { + get => _value; + set => _value = value; + } + + public BeamContentId(string value) + { + _value = value; + } + + public BeamContentId(ContentRef contentRef) : this(contentRef.GetId()) { } + public BeamContentId(ContentObject contentObject) : this(contentObject.Id) { } + + public static implicit operator string(BeamContentId contentId) => contentId.AsString; + public static implicit operator BeamContentId(ContentRef contentRef) => new BeamContentId(contentRef); + public static implicit operator BeamContentId(ContentObject contentObject) => new BeamContentId(contentObject); + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs b/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs new file mode 100644 index 0000000000..5a7cc20f67 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs @@ -0,0 +1,25 @@ +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.ContentManifestId)] + public struct BeamContentManifestId : IBeamSemanticType + { + private string _value; + + public string AsString + { + get => _value; + set => _value = value; + } + + public BeamContentManifestId(string value) + { + _value = value; + } + + public static implicit operator string(BeamContentManifestId id) => id.AsString; + public static implicit operator BeamContentManifestId(string value) => new BeamContentManifestId(value); + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs b/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs new file mode 100644 index 0000000000..4645c253e0 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs @@ -0,0 +1,61 @@ +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.GamerTag)] + public struct BeamGamerTag : IBeamSemanticType + { + private long _longValue; + private string _stringValue; + + public long AsLong + { + get => _longValue; + set { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamGamerTag(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamGamerTag(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamGamerTag tag) => tag.AsString; + public static implicit operator long(BeamGamerTag tag) => tag.AsLong; + + public static implicit operator BeamGamerTag(string value) => new BeamGamerTag(value); + public static implicit operator BeamGamerTag(long value) => new BeamGamerTag(value); + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamPid.cs b/cli/beamable.common/Runtime/Semantics/BeamPid.cs new file mode 100644 index 0000000000..e33c406ac4 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamPid.cs @@ -0,0 +1,25 @@ +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.Pid)] + public struct BeamPid : IBeamSemanticType + { + private string _stringValue; + + public string AsString + { + get => _stringValue; + set => _stringValue = value; + } + + public BeamPid(string value) + { + _stringValue = value; + } + + public static implicit operator string(BeamPid id) => id.AsString; + public static implicit operator BeamPid(string value) => new BeamPid(value); + } +} diff --git a/cli/beamable.common/Runtime/Semantics/BeamStats.cs b/cli/beamable.common/Runtime/Semantics/BeamStats.cs new file mode 100644 index 0000000000..0682c3d2c0 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/BeamStats.cs @@ -0,0 +1,35 @@ +using System; +using Beamable.Common.Api.Stats; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.StatsType)] + public struct BeamStats : IBeamSemanticType + { + private string _value; + + public string AsString + { + get => _value; + set => _value = value; + } + + public BeamStats(StatsDomainType domainType, StatsAccessType accessType, long userId) + { + _value = StatsApiHelper.GeneratePrefix(domainType, accessType, userId); + } + + public BeamStats(string value) + { + _value = value; + } + + public static implicit operator string(BeamStats stats) => stats.AsString; + + public static implicit operator BeamStats((StatsDomainType, StatsAccessType, long) tuple) + { + return new BeamStats(StatsApiHelper.GeneratePrefix(tuple.Item1, tuple.Item2, tuple.Item3)); + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs b/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs new file mode 100644 index 0000000000..d243c748f5 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs @@ -0,0 +1,6 @@ +namespace Beamable.Common.Semantics +{ + public interface IBeamSemanticType + { + } +} diff --git a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamAccountIdConverter.cs b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamAccountIdConverter.cs new file mode 100644 index 0000000000..0ff10f3f0a --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamAccountIdConverter.cs @@ -0,0 +1,36 @@ +using System; +using Newtonsoft.Json; + +namespace Beamable.Common.Semantics.JsonConverters +{ + public class BeamAccountIdConverter : JsonConverter + { + + public override BeamAccountId ReadJson(JsonReader reader, Type objectType, BeamAccountId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamAccountId(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamAccountId(stringValue); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamAccountId"); + } + } + + public override void WriteJson(JsonWriter writer, BeamAccountId value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + + + } +} diff --git a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamCidConverter.cs b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamCidConverter.cs new file mode 100644 index 0000000000..1affd5728a --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamCidConverter.cs @@ -0,0 +1,33 @@ +using System; +using Newtonsoft.Json; + +namespace Beamable.Common.Semantics.JsonConverters +{ + public class BeamCidConverter : JsonConverter + { + public override BeamCid ReadJson(JsonReader reader, Type objectType, BeamCid existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamCid(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamCid(stringValue); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamCid value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamGamerTagConverter.cs b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamGamerTagConverter.cs new file mode 100644 index 0000000000..b99df0e3a5 --- /dev/null +++ b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamGamerTagConverter.cs @@ -0,0 +1,33 @@ +using System; +using Newtonsoft.Json; + +namespace Beamable.Common.Semantics.JsonConverters +{ + public class BeamGamerTagConverter : JsonConverter + { + public override BeamGamerTag ReadJson(JsonReader reader, Type objectType, BeamGamerTag existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamGamerTag(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamGamerTag(stringValue); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamGamerTag"); + } + } + + public override void WriteJson(JsonWriter writer, BeamGamerTag value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + } +} diff --git a/cli/beamable.common/Runtime/Semantics/ServiceName.cs b/cli/beamable.common/Runtime/Semantics/ServiceName.cs index 8767357e31..644230bd19 100644 --- a/cli/beamable.common/Runtime/Semantics/ServiceName.cs +++ b/cli/beamable.common/Runtime/Semantics/ServiceName.cs @@ -4,8 +4,8 @@ namespace Beamable.Common.Semantics { - [CliContractType] - public struct ServiceName + [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.ServiceName)] + public struct ServiceName : IBeamSemanticType { public string Value { get; } public ServiceName(string value) @@ -27,10 +27,13 @@ public ServiceName(string value) } public static implicit operator string(ServiceName d) => d.Value; + public static implicit operator ServiceName(string s) => new ServiceName(s); public override string ToString() { return Value; } + + public Type OpenApiType => typeof(string); } } diff --git a/cli/cli/Contants.cs b/cli/cli/Contants.cs index a9cf3a6daf..48dccf66eb 100644 --- a/cli/cli/Contants.cs +++ b/cli/cli/Contants.cs @@ -19,7 +19,7 @@ public static class Constants /// /// OpenAPI extension that describes the semantic type of a primitive field. /// - public const string EXTENSION_BEAMABLE_SEMANTIC_TYPE = "x-beamable-semantic-type"; + public const string EXTENSION_BEAMABLE_SEMANTIC_TYPE = Beamable.Common.Constants.Features.Services.SCHEMA_SEMANTIC_TYPE_NAME_KEY; /// /// OpenAPI extension, added here as a , diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/Beam.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/Beam.cs index 0f9f0ae29d..9c1cb480ce 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/Commands/Beam.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/Beam.cs @@ -8,7 +8,7 @@ public partial class BeamArgs : Beamable.Common.BeamCli.IBeamCommandArgs { /// [DEPRECATED] Run as much of the command as possible without making any network calls public bool dryrun; - /// CID (CustomerId) to use (found in Portal->Account); defaults to whatever is in '.beamable/connection-configuration.json' + /// CID (CustomerId) to use (found in Portal->Account); defaults to whatever is in '.beamable/config.beam.json' public string cid; /// If passed, sets the engine integration that is calling for the command public string engine; @@ -16,15 +16,15 @@ public partial class BeamArgs : Beamable.Common.BeamCli.IBeamCommandArgs public string engineSdkVersion; /// The version of the engine that is calling the CLI public string engineVersion; - /// PID (Realm ID) to use (found in Portal -> Games -> Any Realm's details); defaults to whatever is in '.beamable/connection-configuration.json' + /// PID (Realm ID) to use (found in Portal -> Games -> Any Realm's details); defaults to whatever is in '.beamable/config.beam.json' public string pid; /// When true, skip input waiting and use default arguments (or error if no defaults are possible) public bool quiet; - /// This option defines the target Beamable environment. Needed for private cloud customers to target their exclusive Beamable environment. Ignorable by everyone else. Stored in '.beamable/connection-configuration.json' + /// This option defines the target Beamable environment. Needed for private cloud customers to target their exclusive Beamable environment. Ignorable by everyone else. Stored in '.beamable/config.beam.json' public string host; - /// The access token to use for the requests. It overwrites the logged in user stored in connection-auth.json for THIS INVOCATION ONLY + /// The access token to use for the requests. It overwrites the logged in user stored in auth.beam.json for THIS INVOCATION ONLY public string accessToken; - /// A Refresh Token to use for the requests. It overwrites the logged in user stored in connection-auth.json for THIS INVOCATION ONLY + /// A Refresh Token to use for the requests. It overwrites the logged in user stored in auth.beam.json for THIS INVOCATION ONLY public string refreshToken; /// Extra logs gets printed out public string log; @@ -47,8 +47,6 @@ public partial class BeamArgs : Beamable.Common.BeamCli.IBeamCommandArgs public bool emitLogStreams; /// additional file paths to be included when building a local project manifest. public string[] addProjectPath; - /// [DEPRECATED] Path override for the .beamable folder - public string dir; /// Output raw JSON to standard out. This happens by default when the command is being piped public bool raw; /// Output syntax highlighted box text. This happens by default when the command is not piped @@ -174,11 +172,6 @@ public virtual string Serialize() genBeamCommandArgs.Add(("--add-project-path=" + this.addProjectPath[i])); } } - // If the dir value was not default, then add it to the list of args. - if ((this.dir != default(string))) - { - genBeamCommandArgs.Add(("--dir=" + this.dir)); - } // If the raw value was not default, then add it to the list of args. if ((this.raw != default(bool))) { diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamInit.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamInit.cs index 0e823d9f5e..9aa1913137 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamInit.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamInit.cs @@ -12,9 +12,9 @@ public partial class InitArgs : Beamable.Common.BeamCli.IBeamCommandArgs public string email; /// User password public string password; - /// This option defines the target Beamable environment. Needed for private cloud customers to target their exclusive Beamable environment. Ignorable by everyone else. Stored in '.beamable/connection-configuration.json' + /// This option defines the target Beamable environment. Needed for private cloud customers to target their exclusive Beamable environment. Ignorable by everyone else. Stored in '.beamable/config.beam.json' public string host; - /// A Refresh Token to use for the requests. It overwrites the logged in user stored in connection-auth.json for THIS INVOCATION ONLY + /// A Refresh Token to use for the requests. It overwrites the logged in user stored in auth.beam.json for THIS INVOCATION ONLY public string refreshToken; /// Ignore the existing pid while initializing public bool ignorePid; diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamLogin.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamLogin.cs index 2f9ca64ed1..911c90afcc 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamLogin.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamLogin.cs @@ -18,7 +18,7 @@ public partial class LoginArgs : Beamable.Common.BeamCli.IBeamCommandArgs public bool noTokenSave; /// Makes the resulting access/refresh token pair be realm scoped instead of the default customer scoped one public bool realmScoped; - /// A Refresh Token to use for the requests. It overwrites the logged in user stored in connection-auth.json for THIS INVOCATION ONLY + /// A Refresh Token to use for the requests. It overwrites the logged in user stored in auth.beam.json for THIS INVOCATION ONLY public string refreshToken; /// Prints out login request response to console public bool printToConsole; diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectGenerateProperties.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectGenerateProperties.cs index 06eb9efa85..55fecc3e44 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectGenerateProperties.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectGenerateProperties.cs @@ -1,67 +1,67 @@ -namespace Beamable.Editor.BeamCli.Commands -{ - using Beamable.Common; - using Beamable.Common.BeamCli; - - public partial class ProjectGeneratePropertiesArgs : Beamable.Common.BeamCli.IBeamCommandArgs - { - /// Where the file will be created - public string output; - /// Beam path to be used. Use BEAM_SOLUTION_DIR to template in $(SolutionDir) - public string beamPath; +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + public partial class ProjectGeneratePropertiesArgs : Beamable.Common.BeamCli.IBeamCommandArgs + { + /// Where the file will be created + public string output; + /// Beam path to be used. Use BEAM_SOLUTION_DIR to template in $(SolutionDir) + public string beamPath; /// The solution path to be used. ///The following values have special meaning and are not treated as paths... - ///- "DIR.PROPS" = $([System.IO.Path]::GetDirectoryName(`$(DirectoryBuildPropsPath)`)) - public string solutionDir; - /// A path relative to the given solution directory, that will be used to store the projects /bin and /obj directories. Note: the given path will have the project's assembly name and the bin or obj folder appended - public string buildDir; - /// Serializes the arguments for command line usage. - public virtual string Serialize() - { - // Create a list of arguments for the command - System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); - // Add the output value to the list of args. - genBeamCommandArgs.Add(this.output.ToString()); - // Add the beamPath value to the list of args. - genBeamCommandArgs.Add(this.beamPath.ToString()); - // Add the solutionDir value to the list of args. - genBeamCommandArgs.Add(this.solutionDir.ToString()); - // If the buildDir value was not default, then add it to the list of args. - if ((this.buildDir != default(string))) - { - genBeamCommandArgs.Add(("--build-dir=" + this.buildDir)); - } - string genBeamCommandStr = ""; - // Join all the args with spaces - genBeamCommandStr = string.Join(" ", genBeamCommandArgs); - return genBeamCommandStr; - } - } - public partial class BeamCommands - { - public virtual ProjectGeneratePropertiesWrapper ProjectGenerateProperties(ProjectGeneratePropertiesArgs generatePropertiesArgs) - { - // Create a list of arguments for the command - System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); - genBeamCommandArgs.Add("beam"); - genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); - genBeamCommandArgs.Add("project"); - genBeamCommandArgs.Add("generate-properties"); - genBeamCommandArgs.Add(generatePropertiesArgs.Serialize()); - // Create an instance of an IBeamCommand - Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); - // Join all the command paths and args into one string - string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); - // Configure the command with the command string - command.SetCommand(genBeamCommandStr); - ProjectGeneratePropertiesWrapper genBeamCommandWrapper = new ProjectGeneratePropertiesWrapper(); - genBeamCommandWrapper.Command = command; - // Return the command! - return genBeamCommandWrapper; - } - } - public partial class ProjectGeneratePropertiesWrapper : Beamable.Common.BeamCli.BeamCommandWrapper - { - } -} + ///- "DIR.PROPS" = $([System.IO.Path]::GetDirectoryName(`$(DirectoryBuildPropsPath)`)) + public string solutionDir; + /// A path relative to the given solution directory, that will be used to store the projects /bin and /obj directories. Note: the given path will have the project's assembly name and the bin or obj folder appended + public string buildDir; + /// Serializes the arguments for command line usage. + public virtual string Serialize() + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + // Add the output value to the list of args. + genBeamCommandArgs.Add(this.output.ToString()); + // Add the beamPath value to the list of args. + genBeamCommandArgs.Add(this.beamPath.ToString()); + // Add the solutionDir value to the list of args. + genBeamCommandArgs.Add(this.solutionDir.ToString()); + // If the buildDir value was not default, then add it to the list of args. + if ((this.buildDir != default(string))) + { + genBeamCommandArgs.Add(("--build-dir=" + this.buildDir)); + } + string genBeamCommandStr = ""; + // Join all the args with spaces + genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + return genBeamCommandStr; + } + } + public partial class BeamCommands + { + public virtual ProjectGeneratePropertiesWrapper ProjectGenerateProperties(ProjectGeneratePropertiesArgs generatePropertiesArgs) + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + genBeamCommandArgs.Add("beam"); + genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); + genBeamCommandArgs.Add("project"); + genBeamCommandArgs.Add("generate-properties"); + genBeamCommandArgs.Add(generatePropertiesArgs.Serialize()); + // Create an instance of an IBeamCommand + Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); + // Join all the command paths and args into one string + string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + // Configure the command with the command string + command.SetCommand(genBeamCommandStr); + ProjectGeneratePropertiesWrapper genBeamCommandWrapper = new ProjectGeneratePropertiesWrapper(); + genBeamCommandWrapper.Command = command; + // Return the command! + return genBeamCommandWrapper; + } + } + public partial class ProjectGeneratePropertiesWrapper : Beamable.Common.BeamCli.BeamCommandWrapper + { + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectNewService.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectNewService.cs index c839944e6f..ebdee6dc90 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectNewService.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectNewService.cs @@ -8,7 +8,7 @@ public partial class ProjectNewServiceArgs : Beamable.Common.BeamCli.IBeamComman { /// Name of the new project public Beamable.Common.Semantics.ServiceName name; - /// The target framework to use for the new project. Defaults to the current dotnet runtime framework. + /// The target framework to use for the new project. Defaults to the current dotnet runtime framework public string targetFramework; /// Relative path to the .sln file to use for the new project. If the .sln file does not exist, it will be created. When no option is configured, if this command is executing inside a .beamable folder, then the first .sln found in .beamable/.. will be used. If no .sln is found, the .sln path will be .sln. If no .beamable folder exists, then the /.sln will be used public string sln; diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectNewStorage.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectNewStorage.cs index f3f4600bf9..0d2496e9c2 100644 --- a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectNewStorage.cs +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectNewStorage.cs @@ -8,7 +8,7 @@ public partial class ProjectNewStorageArgs : Beamable.Common.BeamCli.IBeamComman { /// Name of the new project public Beamable.Common.Semantics.ServiceName name; - /// The target framework to use for the new project. Defaults to the current dotnet runtime framework. + /// The target framework to use for the new project. Defaults to the current dotnet runtime framework public string targetFramework; /// Relative path to the .sln file to use for the new project. If the .sln file does not exist, it will be created. When no option is configured, if this command is executing inside a .beamable folder, then the first .sln found in .beamable/.. will be used. If no .sln is found, the .sln path will be .sln. If no .beamable folder exists, then the /.sln will be used public string sln; diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index acf8e86a6b..fb89053c26 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -8,6 +8,7 @@ using Microsoft.OpenApi.Models; using System.Collections; using System.Reflection; +using Beamable.Common.Semantics; using UnityEngine; using static Beamable.Common.Constants.Features.Services; @@ -38,7 +39,9 @@ public OAPIType(ServiceMethod method, Type type) public bool IsFromBeamGenerateSchema() => SourceCallable == null && Type.GetCustomAttribute() != null; public bool IsFromCallableWithNoClientGen() => IsFromCallable() && SourceCallable.Method.GetCustomAttribute(true).Flags.HasFlag(CallableFlags.SkipGenerateClientFiles); - public bool ShouldNotGenerateClientCode() => (IsFromFederation() || IsFromCallableWithNoClientGen()) && !IsFromBeamGenerateSchema(); + public bool IsBeamSemanticType() => Type.GetCustomAttribute() != null; + + public bool ShouldSkipClientCodeGeneration() => (IsFromFederation() || IsFromCallableWithNoClientGen() || IsBeamSemanticType()) && !IsFromBeamGenerateSchema(); public bool IsPrimitive() { @@ -204,6 +207,10 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti return Convert(x.GetGenericArguments()[0], depth - 1); case { } x when x.IsGenericType && x.GetGenericTypeDefinition() == typeof(Nullable<>): return Convert(x.GetGenericArguments()[0], depth - 1); + case { } x when x.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)): + var semanticType = x.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)); + + return Convert(semanticType.GetGenericArguments()[0], depth - 1); case { } x when x == typeof(double): return new OpenApiSchema { Type = "number", Format = "double" }; case { } x when x == typeof(float): @@ -312,6 +319,23 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti var comment = DocsLoader.GetMemberComments(member); fieldSchema.Description = comment?.Summary; + + // First check if the type has the BeamSemanticTypeAttribute (all IBeamSemanticType have it already) + var classBeamSemanticTypeAttr = member.FieldType.GetCustomAttribute(); + if (classBeamSemanticTypeAttr != null) + { + fieldSchema.Extensions[SCHEMA_SEMANTIC_TYPE_NAME_KEY] = + new OpenApiString(classBeamSemanticTypeAttr.SemanticType); + } + + // Then check if the method declaration has it, if it has, it will override the one from the class + var beamSemanticTypeAttribute = member.GetCustomAttribute(); + if (beamSemanticTypeAttribute != null) + { + fieldSchema.Extensions[SCHEMA_SEMANTIC_TYPE_NAME_KEY] = + new OpenApiString(beamSemanticTypeAttribute.SemanticType); + } + schema.Properties[name] = fieldSchema; if (!member.FieldType.IsAssignableTo(typeof(Optional))) diff --git a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs index 884d0e5c1f..3e086c77fe 100644 --- a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs @@ -30,6 +30,7 @@ public class ServiceDocGenerator private const string V2_COMPONENT_FEDERATION_CLASS_NAME = Constants.Features.Services.MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_CLASS_NAME_KEY; private const string SCHEMA_IS_OPTIONAL_KEY = Constants.Features.Services.SCHEMA_IS_OPTIONAL_KEY; private const string SCHEMA_OPTIONAL_TYPE_KEY = Constants.Features.Services.SCHEMA_OPTIONAL_TYPE_NAME_KEY; + private const string SCHEMA_SEMANTIC_TYPE_NAME_KEY = Constants.Features.Services.SCHEMA_SEMANTIC_TYPE_NAME_KEY; private static OpenApiSecurityScheme _userSecurityScheme = new OpenApiSecurityScheme @@ -203,20 +204,20 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r // That's what this thing does. var type = oapiType.Type; var key = SchemaGenerator.GetQualifiedReferenceName(type); + bool shouldSkipCodeGen = oapiType.ShouldSkipClientCodeGeneration(); if (doc.Components.Schemas.TryGetValue(key, out var existingSchema)) { - var shouldGenerate = !oapiType.ShouldNotGenerateClientCode(); - if (shouldGenerate) existingSchema.AddExtension(Constants.Features.Services.METHOD_SKIP_CLIENT_GENERATION_KEY, new OpenApiBoolean(false)); + existingSchema.AddExtension(Constants.Features.Services.METHOD_SKIP_CLIENT_GENERATION_KEY, new OpenApiBoolean(shouldSkipCodeGen)); - BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Tried to add Schema more than once. Type={type.FullName}, SchemaKey={key}, WillGenClient={oapiType.ShouldNotGenerateClientCode()}"); + BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Tried to add Schema more than once. Type={type.FullName}, SchemaKey={key}, WillSGenClient={!shouldSkipCodeGen}"); } else { // Convert the type into a schema, then set this schema's client-code generation extension based on whether the OAPI type so our code-gen pipelines can decide whether to output it. var schema = SchemaGenerator.Convert(type); - schema.AddExtension(Constants.Features.Services.METHOD_SKIP_CLIENT_GENERATION_KEY, new OpenApiBoolean(oapiType.ShouldNotGenerateClientCode())); + schema.AddExtension(Constants.Features.Services.METHOD_SKIP_CLIENT_GENERATION_KEY, new OpenApiBoolean(shouldSkipCodeGen)); - BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Adding Schema to Microservice OAPI docs. Type={type.FullName}, WillGenClient={oapiType.ShouldNotGenerateClientCode()}"); + BeamableZLoggerProvider.LogContext.Value.ZLogDebug($"Adding Schema to Microservice OAPI docs. Type={type.FullName}, WillGenClient={!shouldSkipCodeGen}"); doc.Components.Schemas.Add(key, schema); } } @@ -231,13 +232,22 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r var parameterNameToComment = comments.Parameters.ToDictionary(kvp => kvp.Name, kvp => kvp.Text); var returnType = GetTypeFromPromiseOrTask(method.Method.ReturnType); - + OpenApiSchema openApiSchema = SchemaGenerator.Convert(returnType, 0); + + var beamSemanticType = method.Method.ReturnParameter.GetCustomAttribute(); + if (beamSemanticType != null) + { + openApiSchema.Extensions.Add(SCHEMA_SEMANTIC_TYPE_NAME_KEY, new OpenApiString(beamSemanticType.SemanticType)); + } + var returnJson = new OpenApiMediaType { Schema = openApiSchema }; if (openApiSchema.Reference != null && !doc.Components.Schemas.ContainsKey(openApiSchema.Reference.Id)) { returnJson.Extensions.Add(Constants.Features.Services.MICROSERVICE_EXTENSION_BEAMABLE_TYPE_ASSEMBLY_QUALIFIED_NAME, new OpenApiString(returnType.GetGenericSanitizedFullName())); } + + var response = new OpenApiResponse() { Description = comments.Returns ?? "", }; if (!IsEmptyResponseType(returnType)) @@ -279,6 +289,12 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r bool isOptional = parameterType.IsAssignableTo(typeof(Optional)); parameterSchema.Nullable = isNullable; parameterSchema.Extensions.Add(SCHEMA_IS_OPTIONAL_KEY, new OpenApiBoolean(isOptional)); + + var paramBeamSemanticType = method.ParameterInfos[i].GetCustomAttribute(); + if (paramBeamSemanticType != null) + { + parameterSchema.Extensions.Add(SCHEMA_SEMANTIC_TYPE_NAME_KEY, new OpenApiString(paramBeamSemanticType.SemanticType)); + } switch (parameterSource) { @@ -489,4 +505,4 @@ public static OpenApiDocument Generate(this ServiceDocGenerator g }; return generator.Generate(startupContext, provider); } -} \ No newline at end of file +} diff --git a/microservice/beamable.tooling.common/UnityJsonContractResolver.cs b/microservice/beamable.tooling.common/UnityJsonContractResolver.cs index a742de86e9..cbc6ee684e 100644 --- a/microservice/beamable.tooling.common/UnityJsonContractResolver.cs +++ b/microservice/beamable.tooling.common/UnityJsonContractResolver.cs @@ -2,6 +2,7 @@ using Beamable.Common.Api.Inventory; using Beamable.Common.Content; using System.Reflection; +using Beamable.Common.Semantics.JsonConverters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; @@ -30,6 +31,9 @@ public class UnitySerializationSettings : JsonSerializerSettings new StringToSomethingDictionaryConverter(), new StringToSomethingDictionaryConverter>(), new OptionalConverter(), + new BeamAccountIdConverter(), + new BeamCidConverter(), + new BeamGamerTagConverter(), // THIS MUST BE LAST, because it is hacky, and falls back onto other converts as its _normal_ behaviour. If its not last, then other converts can run twice, which causes newtonsoft to explode. new UnitySerializationCallbackInvoker(), diff --git a/microservice/microserviceTests/OpenAPITests/TypeTests.cs b/microservice/microserviceTests/OpenAPITests/TypeTests.cs index daba283d61..6967947076 100644 --- a/microservice/microserviceTests/OpenAPITests/TypeTests.cs +++ b/microservice/microserviceTests/OpenAPITests/TypeTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Beamable.Common.Semantics; using UnityEngine; namespace microserviceTests.OpenAPITests; @@ -105,6 +106,21 @@ public void CheckEnumsOnObject() var prop = schema.Properties[nameof(FishThing.type)]; Assert.AreEqual("microserviceTests.OpenAPITests.TypeTests.Fish", prop.Reference.Id); } + + [TestCase(typeof(BeamAccountId), "integer", "int64")] + [TestCase(typeof(BeamCid), "integer", "int64")] + [TestCase(typeof(BeamContentId), "string", null)] + [TestCase(typeof(BeamContentManifestId), "string", null)] + [TestCase(typeof(BeamGamerTag), "integer", "int64")] + [TestCase(typeof(BeamPid), "string", null)] + [TestCase(typeof(BeamStats), "string", null)] + [TestCase(typeof(ServiceName), "string", null)] + public void CheckSemanticTypes(Type type, string typeName, string format) + { + var schema = SchemaGenerator.Convert(type); + Assert.AreEqual(typeName, schema.Type); + Assert.AreEqual(format, schema.Format); + } /// From 74dcc190e3fabb224cc2c67761614406ffa19064 Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Fri, 12 Dec 2025 16:57:31 -0300 Subject: [PATCH 2/9] change: Updated Semantic Type to use types inherits by IBeamSemanticType instead of using an attribute. --- .../Runtime/BeamSemanticTypeAttribute.cs | 33 --- .../Runtime/Semantics/BeamAccountId.cs | 10 +- .../Runtime/Semantics/BeamCid.cs | 13 +- .../Runtime/Semantics/BeamContentId.cs | 11 +- .../Semantics/BeamContentManifestId.cs | 10 +- .../Runtime/Semantics/BeamGamerTag.cs | 12 +- .../Runtime/Semantics/BeamPid.cs | 10 +- .../Runtime/Semantics/BeamStats.cs | 13 +- .../Runtime/Semantics/IBeamSemanticType.cs | 11 +- .../JsonConverters/BeamAccountIdConverter.cs | 36 --- .../JsonConverters/BeamCidConverter.cs | 33 --- .../JsonConverters/BeamGamerTagConverter.cs | 33 --- .../Runtime/Semantics/ServiceName.cs | 10 +- .../Runtime/SmallerJSON/SmallerJSON.cs | 226 +++++++++++++++ cli/beamable.common/beamable.common.csproj | 4 +- .../OpenApiClientCodeGenerator.cs | 84 ++++++ .../Implementations/ServiceConstants.cs | 5 + .../Common/Runtime/Content/ContentObject.cs | 6 +- .../Runtime/Environment/PackageVersion.cs | 5 + .../JsonSerializable/MessagePackStream.cs | 212 ++++++++++++++ .../MessagePackStream.cs.meta | 11 + .../Common/Runtime/Semantics/BeamAccountId.cs | 71 +++++ .../Runtime/Semantics/BeamAccountId.cs.meta | 11 + .../Common/Runtime/Semantics/BeamCid.cs | 70 +++++ .../Common/Runtime/Semantics/BeamCid.cs.meta | 11 + .../Common/Runtime/Semantics/BeamContentId.cs | 40 +++ .../Runtime/Semantics/BeamContentId.cs.meta | 11 + .../Semantics/BeamContentManifestId.cs | 34 +++ .../Semantics/BeamContentManifestId.cs.meta | 11 + .../Common/Runtime/Semantics/BeamGamerTag.cs | 70 +++++ .../Runtime/Semantics/BeamGamerTag.cs.meta | 11 + .../Common/Runtime/Semantics/BeamPid.cs | 34 +++ .../Common/Runtime/Semantics/BeamPid.cs.meta | 11 + .../Common/Runtime/Semantics/BeamStats.cs | 47 +++ .../Runtime/Semantics/BeamStats.cs.meta | 11 + .../Runtime/Semantics/IBeamSemanticType.cs | 16 ++ .../Semantics/IBeamSemanticType.cs.meta | 11 + .../Common/Runtime/Semantics/ServiceName.cs | 15 +- .../Common/Runtime/SmallerJSON/SmallerJSON.cs | 226 +++++++++++++++ .../Resources/versions-default.json | 2 +- .../Runtime/Server/MicroserviceClient.cs | 7 +- .../Microservice/ServiceMethodHelper.cs | 10 +- .../OpenAPI/SchemaGenerator.cs | 33 +-- .../OpenAPI/ServiceDocGenerator.cs | 13 +- .../UnityJsonContractResolver.cs | 269 +++++++++++++++++- 45 files changed, 1636 insertions(+), 197 deletions(-) delete mode 100644 cli/beamable.common/Runtime/BeamSemanticTypeAttribute.cs delete mode 100644 cli/beamable.common/Runtime/Semantics/JsonConverters/BeamAccountIdConverter.cs delete mode 100644 cli/beamable.common/Runtime/Semantics/JsonConverters/BeamCidConverter.cs delete mode 100644 cli/beamable.common/Runtime/Semantics/JsonConverters/BeamGamerTagConverter.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs.meta create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs create mode 100644 client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs.meta diff --git a/cli/beamable.common/Runtime/BeamSemanticTypeAttribute.cs b/cli/beamable.common/Runtime/BeamSemanticTypeAttribute.cs deleted file mode 100644 index 305fa4109c..0000000000 --- a/cli/beamable.common/Runtime/BeamSemanticTypeAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace Beamable -{ - - public enum BeamSemanticType - { - Cid, - Pid, - AccountId, - GamerTag, - ContentManifestId, - ContentId, - StatsType, - ServiceName, - } - - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Class | AttributeTargets.Struct)] - public class BeamSemanticTypeAttribute : Attribute - { - public string SemanticType { get; } - - public BeamSemanticTypeAttribute(BeamSemanticType semanticType) - { - SemanticType = semanticType.ToString(); - } - - public BeamSemanticTypeAttribute(string customSemanticType) - { - SemanticType = customSemanticType; - } - } -} diff --git a/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs b/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs index 39b78dd89f..1ef7a9f4b1 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs @@ -3,12 +3,14 @@ namespace Beamable.Common.Semantics { - [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.AccountId)] + [CliContractType, Serializable] public struct BeamAccountId : IBeamSemanticType { private long _longValue; private string _stringValue; + public string SemanticName => "AccountId"; + public long AsLong { get => _longValue; @@ -21,7 +23,7 @@ public long AsLong public string AsString { - get => _stringValue; + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; set { if (string.IsNullOrEmpty(value)) @@ -58,5 +60,9 @@ public BeamAccountId(string value) public static implicit operator BeamAccountId(string value) => new BeamAccountId(value); public static implicit operator BeamAccountId(long value) => new BeamAccountId(value); + public string ToJson() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamCid.cs b/cli/beamable.common/Runtime/Semantics/BeamCid.cs index a04858f81c..95e73eb12a 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamCid.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamCid.cs @@ -1,14 +1,17 @@ using System; using Beamable.Common.BeamCli; +using Beamable.Serialization.SmallerJSON; namespace Beamable.Common.Semantics { - [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.Cid)] + [CliContractType, Serializable] public struct BeamCid : IBeamSemanticType { - private long _longValue; + private long _longValue; private string _stringValue; + public string SemanticName => "Cid"; + public long AsLong { get => _longValue; set { @@ -19,7 +22,7 @@ public long AsLong { public string AsString { - get => _stringValue; + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; set { if (string.IsNullOrEmpty(value)) @@ -56,5 +59,9 @@ public BeamCid(string value) public static implicit operator BeamCid(string value) => new BeamCid(value); public static implicit operator BeamCid(long value) => new BeamCid(value); + public string ToJson() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamContentId.cs b/cli/beamable.common/Runtime/Semantics/BeamContentId.cs index 09ca8a37d0..5acacc0dde 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamContentId.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamContentId.cs @@ -4,14 +4,16 @@ namespace Beamable.Common.Semantics { - [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.ContentId)] + [CliContractType, Serializable] public struct BeamContentId : IBeamSemanticType { private string _value; + public string SemanticName => "ContentId"; + public string AsString { - get => _value; + get => _value ?? string.Empty; set => _value = value; } @@ -26,5 +28,10 @@ public BeamContentId(ContentObject contentObject) : this(contentObject.Id) { } public static implicit operator string(BeamContentId contentId) => contentId.AsString; public static implicit operator BeamContentId(ContentRef contentRef) => new BeamContentId(contentRef); public static implicit operator BeamContentId(ContentObject contentObject) => new BeamContentId(contentObject); + public static implicit operator BeamContentId(string contentId) => new BeamContentId(contentId); + public string ToJson() + { + return $"\"{AsString}\""; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs b/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs index 5a7cc20f67..5b3200bcc9 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs @@ -3,14 +3,16 @@ namespace Beamable.Common.Semantics { - [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.ContentManifestId)] + [CliContractType, Serializable] public struct BeamContentManifestId : IBeamSemanticType { private string _value; + public string SemanticName => "ContentManifestId"; + public string AsString { - get => _value; + get => _value ?? string.Empty; set => _value = value; } @@ -21,5 +23,9 @@ public BeamContentManifestId(string value) public static implicit operator string(BeamContentManifestId id) => id.AsString; public static implicit operator BeamContentManifestId(string value) => new BeamContentManifestId(value); + public string ToJson() + { + return $"\"{AsString}\""; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs b/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs index 4645c253e0..647691d056 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs @@ -3,12 +3,14 @@ namespace Beamable.Common.Semantics { - [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.GamerTag)] + [CliContractType, Serializable] public struct BeamGamerTag : IBeamSemanticType { private long _longValue; private string _stringValue; - + + public string SemanticName => "GamerTag"; + public long AsLong { get => _longValue; @@ -20,7 +22,7 @@ public long AsLong public string AsString { - get => _stringValue; + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; set { if (string.IsNullOrEmpty(value)) @@ -57,5 +59,9 @@ public BeamGamerTag(string value) public static implicit operator BeamGamerTag(string value) => new BeamGamerTag(value); public static implicit operator BeamGamerTag(long value) => new BeamGamerTag(value); + public string ToJson() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamPid.cs b/cli/beamable.common/Runtime/Semantics/BeamPid.cs index e33c406ac4..6007db2fe9 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamPid.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamPid.cs @@ -3,14 +3,16 @@ namespace Beamable.Common.Semantics { - [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.Pid)] + [CliContractType, Serializable] public struct BeamPid : IBeamSemanticType { private string _stringValue; + public string SemanticName => "Pid"; + public string AsString { - get => _stringValue; + get => _stringValue ?? string.Empty; set => _stringValue = value; } @@ -21,5 +23,9 @@ public BeamPid(string value) public static implicit operator string(BeamPid id) => id.AsString; public static implicit operator BeamPid(string value) => new BeamPid(value); + public string ToJson() + { + return $"\"{AsString}\""; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamStats.cs b/cli/beamable.common/Runtime/Semantics/BeamStats.cs index 0682c3d2c0..25feae1322 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamStats.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamStats.cs @@ -4,14 +4,16 @@ namespace Beamable.Common.Semantics { - [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.StatsType)] + [CliContractType, Serializable] public struct BeamStats : IBeamSemanticType { private string _value; + public string SemanticName => "StatsType"; + public string AsString { - get => _value; + get => _value ?? string.Empty; set => _value = value; } @@ -31,5 +33,12 @@ public static implicit operator BeamStats((StatsDomainType, StatsAccessType, lon { return new BeamStats(StatsApiHelper.GeneratePrefix(tuple.Item1, tuple.Item2, tuple.Item3)); } + + public static implicit operator BeamStats(string value) => new BeamStats(value); + + public string ToJson() + { + return $"\"{AsString}\""; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs b/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs index d243c748f5..e362105323 100644 --- a/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs +++ b/cli/beamable.common/Runtime/Semantics/IBeamSemanticType.cs @@ -1,6 +1,13 @@ -namespace Beamable.Common.Semantics +using Beamable.Serialization.SmallerJSON; + +namespace Beamable.Common.Semantics { - public interface IBeamSemanticType + public interface IBeamSemanticType : IRawJsonProvider + { + string SemanticName { get; } + } + + public interface IBeamSemanticType : IBeamSemanticType { } } diff --git a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamAccountIdConverter.cs b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamAccountIdConverter.cs deleted file mode 100644 index 0ff10f3f0a..0000000000 --- a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamAccountIdConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Beamable.Common.Semantics.JsonConverters -{ - public class BeamAccountIdConverter : JsonConverter - { - - public override BeamAccountId ReadJson(JsonReader reader, Type objectType, BeamAccountId existingValue, bool hasExistingValue, - JsonSerializer serializer) - { - switch (reader.TokenType) - { - case JsonToken.Integer: - { - var longValue = Convert.ToInt64(reader.Value); - return new BeamAccountId(longValue); - } - case JsonToken.String: - { - var stringValue = (string)reader.Value; - return new BeamAccountId(stringValue); - } - default: - throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamAccountId"); - } - } - - public override void WriteJson(JsonWriter writer, BeamAccountId value, JsonSerializer serializer) - { - writer.WriteValue(value.AsLong); - } - - - } -} diff --git a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamCidConverter.cs b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamCidConverter.cs deleted file mode 100644 index 1affd5728a..0000000000 --- a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamCidConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Beamable.Common.Semantics.JsonConverters -{ - public class BeamCidConverter : JsonConverter - { - public override BeamCid ReadJson(JsonReader reader, Type objectType, BeamCid existingValue, bool hasExistingValue, - JsonSerializer serializer) - { - switch (reader.TokenType) - { - case JsonToken.Integer: - { - var longValue = Convert.ToInt64(reader.Value); - return new BeamCid(longValue); - } - case JsonToken.String: - { - var stringValue = (string)reader.Value; - return new BeamCid(stringValue); - } - default: - throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); - } - } - - public override void WriteJson(JsonWriter writer, BeamCid value, JsonSerializer serializer) - { - writer.WriteValue(value.AsLong); - } - } -} diff --git a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamGamerTagConverter.cs b/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamGamerTagConverter.cs deleted file mode 100644 index b99df0e3a5..0000000000 --- a/cli/beamable.common/Runtime/Semantics/JsonConverters/BeamGamerTagConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Beamable.Common.Semantics.JsonConverters -{ - public class BeamGamerTagConverter : JsonConverter - { - public override BeamGamerTag ReadJson(JsonReader reader, Type objectType, BeamGamerTag existingValue, bool hasExistingValue, - JsonSerializer serializer) - { - switch (reader.TokenType) - { - case JsonToken.Integer: - { - var longValue = Convert.ToInt64(reader.Value); - return new BeamGamerTag(longValue); - } - case JsonToken.String: - { - var stringValue = (string)reader.Value; - return new BeamGamerTag(stringValue); - } - default: - throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamGamerTag"); - } - } - - public override void WriteJson(JsonWriter writer, BeamGamerTag value, JsonSerializer serializer) - { - writer.WriteValue(value.AsLong); - } - } -} diff --git a/cli/beamable.common/Runtime/Semantics/ServiceName.cs b/cli/beamable.common/Runtime/Semantics/ServiceName.cs index 644230bd19..c8f9af5bb8 100644 --- a/cli/beamable.common/Runtime/Semantics/ServiceName.cs +++ b/cli/beamable.common/Runtime/Semantics/ServiceName.cs @@ -4,10 +4,13 @@ namespace Beamable.Common.Semantics { - [CliContractType, Serializable, BeamSemanticType(BeamSemanticType.ServiceName)] + [CliContractType, Serializable] public struct ServiceName : IBeamSemanticType { public string Value { get; } + + public string SemanticName => "ServiceName"; + public ServiceName(string value) { // if we do not set the value skip checks @@ -34,6 +37,11 @@ public override string ToString() return Value; } + public string ToJson() + { + return $"\"{Value}\""; + } + public Type OpenApiType => typeof(string); } } diff --git a/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs b/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs index d02732cf4d..3d49d79a14 100644 --- a/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs +++ b/cli/beamable.common/Runtime/SmallerJSON/SmallerJSON.cs @@ -77,6 +77,35 @@ public static object Deserialize(string json) return obj; } } + + /// + /// Deserialize JSON into a concrete type. Unlike Unity JsonUtility, this supports + /// custom semantic wrapper types (ex: types implementing IBeamSemanticType) + /// even when JSON contains only the primitive token (string/number). + /// + public static T Deserialize(string json) + { + return (T)Deserialize(json, typeof(T)); + } + + /// + /// Deserialize JSON into a concrete type (runtime type overload). + /// + public static object Deserialize(string json, Type targetType) + { + if (json == null) return null; + + // First parse into SmallerJSON's object model (ArrayDict/List/primitives). + object root; + using (var parser = new StringBasedParser(json)) + { + root = parser.ParseValue(); + } + + // Then materialize into the requested type. + return ObjectMapper.ConvertToType(root, targetType); + } + public static bool IsValidJson(string strInput) { @@ -1049,5 +1078,202 @@ private static void SerializeOther(object value, StringBuilder builder) } } } + + private static class ObjectMapper + { + private static readonly Type SerializeFieldType = typeof(SerializeField); + private static readonly Type CompilerGeneratedType = typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute); + + // NOTE: We cannot reference Beamable.Common.Semantics directly from this folder. + // We detect semantic types via reflection by interface full name instead + private const string BeamSemanticInterfaceFullName = "Beamable.Common.Semantics.IBeamSemanticType`1"; + + public static object ConvertToType(object node, Type targetType) + { + if (targetType == null) throw new ArgumentNullException(nameof(targetType)); + + // Null handling + if (node == null) + { + targetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + } + + // Unwrap nullable + targetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + // Semantic types: construct from primitive JSON tokens + if (TryGetSemanticPrimitiveType(targetType, out var primitiveType)) + { + return ConstructSemantic(targetType, primitiveType, node); + } + + // Direct assign + if (targetType.IsInstanceOfType(node)) + { + return node; + } + + // Enums + if (targetType.IsEnum) + { + if (node is string s) return Enum.Parse(targetType, s, ignoreCase: true); + if (node is long l) return Enum.ToObject(targetType, (int)l); + } + + // Common scalars + if (targetType == typeof(string)) return node.ToString(); + if (targetType == typeof(bool)) return Convert.ToBoolean(node); + if (targetType == typeof(long)) return Convert.ToInt64(node); + if (targetType == typeof(int)) return Convert.ToInt32(node); + if (targetType == typeof(double)) return Convert.ToDouble(node); + if (targetType == typeof(float)) return Convert.ToSingle(node); + if (targetType == typeof(decimal)) return Convert.ToDecimal(node); + + // Dictionaries (only generic Dictionary supported here) + if (typeof(IDictionary).IsAssignableFrom(targetType) && targetType.IsGenericType) + { + var genArgs = targetType.GetGenericArguments(); + if (genArgs.Length == 2 && genArgs[0] == typeof(string) && node is ArrayDict dictNode) + { + var valueType = genArgs[1]; + var dict = (IDictionary)Activator.CreateInstance(targetType); + foreach (var kvp in dictNode) + { + dict[kvp.Key] = ConvertToType(kvp.Value, valueType); + } + return dict; + } + } + + // Arrays + if (targetType.IsArray && node is IList listNodeForArray) + { + var elemType = targetType.GetElementType(); + var arr = Array.CreateInstance(elemType!, listNodeForArray.Count); + for (int i = 0; i < listNodeForArray.Count; i++) + { + arr.SetValue(ConvertToType(listNodeForArray[i], elemType!), i); + } + return arr; + } + + // Lists + if (typeof(IList).IsAssignableFrom(targetType) && node is IList listNode) + { + var elemType = targetType.IsGenericType ? targetType.GetGenericArguments()[0] : typeof(object); + var list = (IList)Activator.CreateInstance(targetType); + foreach (var elem in listNode) + { + list.Add(ConvertToType(elem, elemType)); + } + return list; + } + + // POCO object + if (node is ArrayDict objNode) + { + var instance = Activator.CreateInstance(targetType); + + // Mirror existing Serializer rules: fields only, allow [SerializeField] privates, + // skip [NonSerialized] and skip compiler generated backing fields. + var fields = targetType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + for (int i = 0; i < fields.Length; i++) + { + var field = fields[i]; + + var hasSerializeField = field.GetCustomAttribute(SerializeFieldType) != null; + var isGeneratedByCompiler = field.GetCustomAttribute(CompilerGeneratedType) != null; + var canBeSerialized = (hasSerializeField || !field.IsNotSerialized) && !isGeneratedByCompiler; + if (!canBeSerialized) continue; + + if (!objNode.TryGetValue(field.Name, out var rawValue)) continue; + + var converted = ConvertToType(rawValue, field.FieldType); + field.SetValue(instance, converted); + } + + // If the target uses Unity callbacks, respect them (parity with existing Serializer behavior) + if (instance is ISerializationCallbackReceiver receiver) + { + receiver.OnAfterDeserialize(); + } + + return instance; + } + + throw new InvalidOperationException( + $"SmallerJSON cannot convert node type [{node.GetType().FullName}] to [{targetType.FullName}]."); + } + + private static bool TryGetSemanticPrimitiveType(Type semanticType, out Type primitiveType) + { + var iface = semanticType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition().FullName == BeamSemanticInterfaceFullName); + + if (iface == null) + { + primitiveType = null; + return false; + } + + primitiveType = iface.GetGenericArguments()[0]; + return true; + } + + private static object ConstructSemantic(Type semanticType, Type primitiveType, object node) + { + // Prefer ctor(string) when JSON is a string. Many long-backed semantic types accept string too + // and preserve formatting (leading zeros etc) or validate (ServiceName). + if (node is string s) + { + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor != null) return stringCtor.Invoke(new object[] { s }); + + if (primitiveType == typeof(long)) + { + if (!long.TryParse(s, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out var parsed)) + { + throw new FormatException($"Cannot parse '{s}' as long for semantic type [{semanticType.FullName}]."); + } + + var longCtor = semanticType.GetConstructor(new[] { typeof(long) }); + if (longCtor != null) return longCtor.Invoke(new object[] { parsed }); + } + + throw new MissingMethodException( + $"Semantic type [{semanticType.FullName}] has no public ctor(string) and cannot be built from JSON string."); + } + + // If JSON is numeric and semantic primitive is long + if (primitiveType == typeof(long)) + { + var l = Convert.ToInt64(node, System.Globalization.CultureInfo.InvariantCulture); + var longCtor = semanticType.GetConstructor(new[] { typeof(long) }); + if (longCtor != null) return longCtor.Invoke(new object[] { l }); + + // Some semantic types might only expose ctor(string); allow numeric -> string as fallback. + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor != null) return stringCtor.Invoke(new object[] { l.ToString(System.Globalization.CultureInfo.InvariantCulture) }); + + throw new MissingMethodException($"Semantic type [{semanticType.FullName}] must have ctor(long) or ctor(string)."); + } + + // If semantic primitive is string, coerce to string + if (primitiveType == typeof(string)) + { + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor == null) + throw new MissingMethodException($"Semantic type [{semanticType.FullName}] must have ctor(string)."); + + return stringCtor.Invoke(new object[] { node.ToString() }); + } + + throw new NotSupportedException( + $"Semantic type [{semanticType.FullName}] uses unsupported primitive [{primitiveType.FullName}]."); + } + } + } } diff --git a/cli/beamable.common/beamable.common.csproj b/cli/beamable.common/beamable.common.csproj index b9b5624619..e3b789840c 100644 --- a/cli/beamable.common/beamable.common.csproj +++ b/cli/beamable.common/beamable.common.csproj @@ -55,10 +55,10 @@ - + diff --git a/cli/cli/Services/UnityOpenApiSourceGenerator/OpenApiClientCodeGenerator.cs b/cli/cli/Services/UnityOpenApiSourceGenerator/OpenApiClientCodeGenerator.cs index cad39b49f8..0b315a68f6 100644 --- a/cli/cli/Services/UnityOpenApiSourceGenerator/OpenApiClientCodeGenerator.cs +++ b/cli/cli/Services/UnityOpenApiSourceGenerator/OpenApiClientCodeGenerator.cs @@ -1,13 +1,19 @@ using Beamable.Common; using Beamable.Common.Dependencies; +using Beamable.Common.Semantics; using Beamable.Server.Common; +using Beamable.Tooling.Common.OpenAPI; using Beamable.Tooling.Common.OpenAPI.Utils; using cli; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using System; using System.CodeDom; using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Text; using TypeAttributes = System.Reflection.TypeAttributes; using ServiceConstants = Beamable.Common.Constants.Features.Services; @@ -434,9 +440,44 @@ private void AddCallableMethod(CodeTypeDeclaration targetClass, string methodNam targetClass.Members.Add(genMethod); } + private bool IsSemanticType(OpenApiSchema schema, out string semanticType) + { + semanticType = string.Empty; + if (!schema.Extensions.TryGetValue(ServiceConstants.SCHEMA_SEMANTIC_TYPE_NAME_KEY, + out var semanticTypeName) || semanticTypeName is not OpenApiString semanticTypeNameStr) + { + return false; + } + semanticType = semanticTypeNameStr.Value; + return true; + } + private string GetParsedType(OpenApiSchema schema, bool useFullName = false) { + if (IsSemanticType(schema, out var semanticType)) + { + switch (semanticType) + { + case "Cid": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamCid)); + case "Pid": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamPid)); + case "AccountId": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamAccountId)); + case "GamerTag": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamGamerTag)); + case "ContentManifestId": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamContentManifestId)); + case "ContentId": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamContentId)); + case "StatsType": + return SchemaGenerator.GetQualifiedReferenceName(typeof(BeamStats)); + case "ServiceName": + return SchemaGenerator.GetQualifiedReferenceName(typeof(ServiceName)); + } + } + if (schema.Extensions.TryGetValue(ServiceConstants.MICROSERVICE_EXTENSION_BEAMABLE_FORCE_TYPE_NAME, out var extensionType) && extensionType is OpenApiString forcedTypeName) { @@ -554,6 +595,49 @@ private string GetParameterTypeNameBase(OpenApiSchema schema, bool isNullable) private string GetParameterTypeName(OpenApiSchema schema) { + if (!IsSemanticType(schema, out var semanticType)) + { + return (schema.Type, schema.Format) switch + { + ("integer", "int16") => typeof(short).GetTypeString(), + ("integer", "int32") => typeof(int).GetTypeString(), + ("integer", "int64") => typeof(long).GetTypeString(), + ("integer", _) => typeof(int).GetTypeString(), + ("number", "float") => typeof(float).GetTypeString(), + ("number", "double") => typeof(double).GetTypeString(), + ("number", "decimal") => typeof(decimal).GetTypeString(), + ("number", _) => typeof(decimal).GetTypeString(), + ("string", "date") => typeof(DateTime).GetTypeString(), + ("string", "date-time") => typeof(DateTime).GetTypeString(), + ("string", "uuid") => typeof(Guid).GetTypeString(), + ("string", "byte") => typeof(byte).GetTypeString(), + ("string", _) => typeof(string).GetTypeString(), + ("boolean", _) => typeof(bool).GetTypeString(), + ("array", _) => GetParameterArrayTypeName(schema), + _ => GetObjectType(schema) + }; + } + + switch (semanticType) + { + case "Cid": + return nameof(BeamCid); + case "Pid": + return nameof(BeamPid); + case "AccountId": + return nameof(BeamAccountId); + case "GamerTag": + return nameof(BeamGamerTag); + case "ContentManifestId": + return nameof(BeamContentManifestId); + case "ContentId": + return nameof(BeamContentId); + case "StatsType": + return nameof(BeamStats); + case "ServiceName": + return nameof(ServiceName); + } + return (schema.Type, schema.Format) switch { ("integer", "int16") => typeof(short).GetTypeString(), diff --git a/client/Packages/com.beamable/Common/Runtime/Constants/Implementations/ServiceConstants.cs b/client/Packages/com.beamable/Common/Runtime/Constants/Implementations/ServiceConstants.cs index 531cc17816..5657c5c2c6 100644 --- a/client/Packages/com.beamable/Common/Runtime/Constants/Implementations/ServiceConstants.cs +++ b/client/Packages/com.beamable/Common/Runtime/Constants/Implementations/ServiceConstants.cs @@ -82,6 +82,11 @@ public static partial class Services public const string PATH_CALLABLE_METHOD_NAME_KEY = "x-beamable-callable-method-name"; public const string PATH_CALLABLE_METHOD_CLIENT_PREFIX_KEY = "x-beamable-route-source-client-prefix"; + /// + /// OpenAPI extension that describes the semantic type of a primitive field. + /// + public const string SCHEMA_SEMANTIC_TYPE_NAME_KEY = "x-beamable-semantic-type"; + public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_INTERFACE_KEY = "interface"; public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_ID_KEY = "federationId"; public const string MICROSERVICE_FEDERATED_COMPONENTS_V2_FEDERATION_CLASS_NAME_KEY = "federationClassName"; diff --git a/client/Packages/com.beamable/Common/Runtime/Content/ContentObject.cs b/client/Packages/com.beamable/Common/Runtime/Content/ContentObject.cs index 9fd9773afe..4050730ff1 100644 --- a/client/Packages/com.beamable/Common/Runtime/Content/ContentObject.cs +++ b/client/Packages/com.beamable/Common/Runtime/Content/ContentObject.cs @@ -1,6 +1,6 @@ -// This file generated by a copy-operation from another project. -// Edits to this file will be overwritten by the build process. - +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + using Beamable.Common.BeamCli.Contracts; using Beamable.Common.Content.Serialization; using Beamable.Common.Content.Validation; diff --git a/client/Packages/com.beamable/Common/Runtime/Environment/PackageVersion.cs b/client/Packages/com.beamable/Common/Runtime/Environment/PackageVersion.cs index 3178356495..c704d4ef31 100644 --- a/client/Packages/com.beamable/Common/Runtime/Environment/PackageVersion.cs +++ b/client/Packages/com.beamable/Common/Runtime/Environment/PackageVersion.cs @@ -81,6 +81,11 @@ public class PackageVersion /// public int? RC => IsReleaseCandidate ? _rc : default; + /// + /// Whether this version is one of our locally built versions relative to our internal workflow. + /// + public bool IsLocalDev => ToString().StartsWith("0.0.123"); + public PackageVersion(int major, int minor, int patch, int rc = -1, long nightlyTime = -1, bool isPreview = false, bool isExperimental = false) { _major = major; diff --git a/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs b/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs new file mode 100644 index 0000000000..90050afcf7 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs @@ -0,0 +1,212 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Beamable.Common.Pooling; +using UnityEngine; + +namespace Beamable.Serialization +{ + public static class BeamMessagePack + { + public static byte[] Serialize(JsonSerializable.ISerializable serializable) + { + var stream = new MessagePackSerializerStream(); + serializable.Serialize(stream); + + return stream._bytes.ToArray(); + } + } + + public class MessagePackSerializerStream : JsonSerializable.IStreamSerializer + { + public List _bytes = new List(1000); + + public bool isSaving { get; } + public bool isLoading { get; } + public object GetValue(string key) + { + throw new NotImplementedException(); + } + + public void SetValue(string key, object value) + { + throw new NotImplementedException(); + } + + public bool HasKey(string key) + { + throw new NotImplementedException(); + } + + public JsonSerializable.ListMode Mode { get; } + public bool SerializeNestedJson(string key, ref JsonString jsonString) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref IDictionary target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref bool target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref bool? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref int target) + { + // throw new NotImplementedException(); + return true; + } + + public bool Serialize(string key, ref int? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref long target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref long? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref ulong target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref ulong? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref float target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref float? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref double target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref double? target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref string target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Guid target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref StringBuilder target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref DateTime target, params string[] formats) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Rect target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Vector2 target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Vector3 target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Vector4 target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Color target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Quaternion target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref Gradient target) + { + throw new NotImplementedException(); + } + + public bool Serialize(string key, ref T value) where T : JsonSerializable.ISerializable + { + throw new NotImplementedException(); + } + + public bool SerializeInline(string key, ref T value) where T : JsonSerializable.ISerializable + { + throw new NotImplementedException(); + } + + public bool SerializeList(string key, ref TList value) where TList : IList, new() + { + throw new NotImplementedException(); + } + + public bool SerializeKnownList(string key, ref List value) where TElem : JsonSerializable.ISerializable, new() + { + throw new NotImplementedException(); + } + + public bool SerializeArray(string key, ref T[] value) + { + throw new NotImplementedException(); + } + + public bool SerializeDictionary(string key, ref Dictionary target) + { + throw new NotImplementedException(); + } + + public bool SerializeDictionary(string key, ref TDict target) where TDict : IDictionary, new() + { + throw new NotImplementedException(); + } + + public bool SerializeILL(string key, ref LinkedList list) where T : ClassPool, new() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs.meta b/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs.meta new file mode 100644 index 0000000000..aa9cb8115c --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/JsonSerializable/MessagePackStream.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80b70a8446bc7d3a881cedef86cb9300 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs new file mode 100644 index 0000000000..969f40f297 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs @@ -0,0 +1,71 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using Beamable.Common.BeamCli; +using System; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamAccountId : IBeamSemanticType + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "AccountId"; + + public long AsLong + { + get => _longValue; + set + { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamAccountId(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamAccountId(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamAccountId id) => id.AsString; + public static implicit operator long(BeamAccountId id) => id.AsLong; + + public static implicit operator BeamAccountId(string value) => new BeamAccountId(value); + public static implicit operator BeamAccountId(long value) => new BeamAccountId(value); + public string ToJson() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs.meta new file mode 100644 index 0000000000..ec5fc38126 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ce2f2d73de9b7005aba6633a37c270e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs new file mode 100644 index 0000000000..53e53ae424 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs @@ -0,0 +1,70 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; +using Beamable.Serialization.SmallerJSON; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamCid : IBeamSemanticType + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "Cid"; + + public long AsLong { + get => _longValue; + set { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamCid(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamCid(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamCid cid) => cid.AsString; + public static implicit operator long(BeamCid cid) => cid.AsLong; + + public static implicit operator BeamCid(string value) => new BeamCid(value); + public static implicit operator BeamCid(long value) => new BeamCid(value); + public string ToJson() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs.meta new file mode 100644 index 0000000000..6e1afd393d --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3cfc98afee87b127c6b2f03388554840 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs new file mode 100644 index 0000000000..71718c1579 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs @@ -0,0 +1,40 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; +using Beamable.Common.Content; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamContentId : IBeamSemanticType + { + private string _value; + + public string SemanticName => "ContentId"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamContentId(string value) + { + _value = value; + } + + public BeamContentId(ContentRef contentRef) : this(contentRef.GetId()) { } + public BeamContentId(ContentObject contentObject) : this(contentObject.Id) { } + + public static implicit operator string(BeamContentId contentId) => contentId.AsString; + public static implicit operator BeamContentId(ContentRef contentRef) => new BeamContentId(contentRef); + public static implicit operator BeamContentId(ContentObject contentObject) => new BeamContentId(contentObject); + public static implicit operator BeamContentId(string contentId) => new BeamContentId(contentId); + public string ToJson() + { + return $"\"{AsString}\""; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs.meta new file mode 100644 index 0000000000..7eae704b9f --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85f785bd8a78971ca7bcb5a9658917a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs new file mode 100644 index 0000000000..956845d5d3 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs @@ -0,0 +1,34 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamContentManifestId : IBeamSemanticType + { + private string _value; + + public string SemanticName => "ContentManifestId"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamContentManifestId(string value) + { + _value = value; + } + + public static implicit operator string(BeamContentManifestId id) => id.AsString; + public static implicit operator BeamContentManifestId(string value) => new BeamContentManifestId(value); + public string ToJson() + { + return $"\"{AsString}\""; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs.meta new file mode 100644 index 0000000000..efaa8acce9 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f0402b24d33d592f9720aa3eae4ffafc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs new file mode 100644 index 0000000000..647e424e95 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs @@ -0,0 +1,70 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamGamerTag : IBeamSemanticType + { + private long _longValue; + private string _stringValue; + + public string SemanticName => "GamerTag"; + + public long AsLong + { + get => _longValue; + set { + _longValue = value; + _stringValue = value.ToString(); + } + } + + public string AsString + { + get => string.IsNullOrEmpty(_stringValue) ? _longValue.ToString() : _stringValue; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + } + + public BeamGamerTag(long value) + { + _longValue = value; + _stringValue = value.ToString(); + } + + public BeamGamerTag(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException($"Parameter {nameof(value)} cannot be null or empty."); + } + _stringValue = value; + _longValue = long.TryParse(value, out var longValue) + ? longValue + : throw new ArgumentException($"Parameter {nameof(value)} is invalid. Must be a numeric value."); + } + + public static implicit operator string(BeamGamerTag tag) => tag.AsString; + public static implicit operator long(BeamGamerTag tag) => tag.AsLong; + + public static implicit operator BeamGamerTag(string value) => new BeamGamerTag(value); + public static implicit operator BeamGamerTag(long value) => new BeamGamerTag(value); + public string ToJson() + { + return AsString; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs.meta new file mode 100644 index 0000000000..700fa21ec6 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 68b6c25ddc6a884b0a5924d3e0b1137b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs new file mode 100644 index 0000000000..4792d17901 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs @@ -0,0 +1,34 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamPid : IBeamSemanticType + { + private string _stringValue; + + public string SemanticName => "Pid"; + + public string AsString + { + get => _stringValue ?? string.Empty; + set => _stringValue = value; + } + + public BeamPid(string value) + { + _stringValue = value; + } + + public static implicit operator string(BeamPid id) => id.AsString; + public static implicit operator BeamPid(string value) => new BeamPid(value); + public string ToJson() + { + return $"\"{AsString}\""; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs.meta new file mode 100644 index 0000000000..f6b378a560 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 697b5c66f91cf6c780e86f3125a244a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs new file mode 100644 index 0000000000..44a02cb715 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs @@ -0,0 +1,47 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using System; +using Beamable.Common.Api.Stats; +using Beamable.Common.BeamCli; + +namespace Beamable.Common.Semantics +{ + [CliContractType, Serializable] + public struct BeamStats : IBeamSemanticType + { + private string _value; + + public string SemanticName => "StatsType"; + + public string AsString + { + get => _value ?? string.Empty; + set => _value = value; + } + + public BeamStats(StatsDomainType domainType, StatsAccessType accessType, long userId) + { + _value = StatsApiHelper.GeneratePrefix(domainType, accessType, userId); + } + + public BeamStats(string value) + { + _value = value; + } + + public static implicit operator string(BeamStats stats) => stats.AsString; + + public static implicit operator BeamStats((StatsDomainType, StatsAccessType, long) tuple) + { + return new BeamStats(StatsApiHelper.GeneratePrefix(tuple.Item1, tuple.Item2, tuple.Item3)); + } + + public static implicit operator BeamStats(string value) => new BeamStats(value); + + public string ToJson() + { + return $"\"{AsString}\""; + } + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs.meta new file mode 100644 index 0000000000..20e7eacdea --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 276d4bec5854f140d7d336ea74f90bcc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs new file mode 100644 index 0000000000..befa54be04 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs @@ -0,0 +1,16 @@ +// This file generated by a copy-operation from another project. +// Edits to this file will be overwritten by the build process. + +using Beamable.Serialization.SmallerJSON; + +namespace Beamable.Common.Semantics +{ + public interface IBeamSemanticType : IRawJsonProvider + { + string SemanticName { get; } + } + + public interface IBeamSemanticType : IBeamSemanticType + { + } +} diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs.meta b/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs.meta new file mode 100644 index 0000000000..fffc8d3a67 --- /dev/null +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/IBeamSemanticType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e7bf0d59ed76ceb5837ec008a785c9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs index 1c8c122d79..45441462d2 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs @@ -7,10 +7,13 @@ namespace Beamable.Common.Semantics { - [CliContractType] - public struct ServiceName + [CliContractType, Serializable] + public struct ServiceName : IBeamSemanticType { public string Value { get; } + + public string SemanticName => "ServiceName"; + public ServiceName(string value) { // if we do not set the value skip checks @@ -30,10 +33,18 @@ public ServiceName(string value) } public static implicit operator string(ServiceName d) => d.Value; + public static implicit operator ServiceName(string s) => new ServiceName(s); public override string ToString() { return Value; } + + public string ToJson() + { + return $"\"{Value}\""; + } + + public Type OpenApiType => typeof(string); } } diff --git a/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs b/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs index a1b609d00f..176f253779 100644 --- a/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs +++ b/client/Packages/com.beamable/Common/Runtime/SmallerJSON/SmallerJSON.cs @@ -80,6 +80,35 @@ public static object Deserialize(string json) return obj; } } + + /// + /// Deserialize JSON into a concrete type. Unlike Unity JsonUtility, this supports + /// custom semantic wrapper types (ex: types implementing IBeamSemanticType) + /// even when JSON contains only the primitive token (string/number). + /// + public static T Deserialize(string json) + { + return (T)Deserialize(json, typeof(T)); + } + + /// + /// Deserialize JSON into a concrete type (runtime type overload). + /// + public static object Deserialize(string json, Type targetType) + { + if (json == null) return null; + + // First parse into SmallerJSON's object model (ArrayDict/List/primitives). + object root; + using (var parser = new StringBasedParser(json)) + { + root = parser.ParseValue(); + } + + // Then materialize into the requested type. + return ObjectMapper.ConvertToType(root, targetType); + } + public static bool IsValidJson(string strInput) { @@ -1052,5 +1081,202 @@ private static void SerializeOther(object value, StringBuilder builder) } } } + + private static class ObjectMapper + { + private static readonly Type SerializeFieldType = typeof(SerializeField); + private static readonly Type CompilerGeneratedType = typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute); + + // NOTE: We cannot reference Beamable.Common.Semantics directly from this folder. + // We detect semantic types via reflection by interface full name instead + private const string BeamSemanticInterfaceFullName = "Beamable.Common.Semantics.IBeamSemanticType`1"; + + public static object ConvertToType(object node, Type targetType) + { + if (targetType == null) throw new ArgumentNullException(nameof(targetType)); + + // Null handling + if (node == null) + { + targetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; + } + + // Unwrap nullable + targetType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + // Semantic types: construct from primitive JSON tokens + if (TryGetSemanticPrimitiveType(targetType, out var primitiveType)) + { + return ConstructSemantic(targetType, primitiveType, node); + } + + // Direct assign + if (targetType.IsInstanceOfType(node)) + { + return node; + } + + // Enums + if (targetType.IsEnum) + { + if (node is string s) return Enum.Parse(targetType, s, ignoreCase: true); + if (node is long l) return Enum.ToObject(targetType, (int)l); + } + + // Common scalars + if (targetType == typeof(string)) return node.ToString(); + if (targetType == typeof(bool)) return Convert.ToBoolean(node); + if (targetType == typeof(long)) return Convert.ToInt64(node); + if (targetType == typeof(int)) return Convert.ToInt32(node); + if (targetType == typeof(double)) return Convert.ToDouble(node); + if (targetType == typeof(float)) return Convert.ToSingle(node); + if (targetType == typeof(decimal)) return Convert.ToDecimal(node); + + // Dictionaries (only generic Dictionary supported here) + if (typeof(IDictionary).IsAssignableFrom(targetType) && targetType.IsGenericType) + { + var genArgs = targetType.GetGenericArguments(); + if (genArgs.Length == 2 && genArgs[0] == typeof(string) && node is ArrayDict dictNode) + { + var valueType = genArgs[1]; + var dict = (IDictionary)Activator.CreateInstance(targetType); + foreach (var kvp in dictNode) + { + dict[kvp.Key] = ConvertToType(kvp.Value, valueType); + } + return dict; + } + } + + // Arrays + if (targetType.IsArray && node is IList listNodeForArray) + { + var elemType = targetType.GetElementType(); + var arr = Array.CreateInstance(elemType!, listNodeForArray.Count); + for (int i = 0; i < listNodeForArray.Count; i++) + { + arr.SetValue(ConvertToType(listNodeForArray[i], elemType!), i); + } + return arr; + } + + // Lists + if (typeof(IList).IsAssignableFrom(targetType) && node is IList listNode) + { + var elemType = targetType.IsGenericType ? targetType.GetGenericArguments()[0] : typeof(object); + var list = (IList)Activator.CreateInstance(targetType); + foreach (var elem in listNode) + { + list.Add(ConvertToType(elem, elemType)); + } + return list; + } + + // POCO object + if (node is ArrayDict objNode) + { + var instance = Activator.CreateInstance(targetType); + + // Mirror existing Serializer rules: fields only, allow [SerializeField] privates, + // skip [NonSerialized] and skip compiler generated backing fields. + var fields = targetType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + for (int i = 0; i < fields.Length; i++) + { + var field = fields[i]; + + var hasSerializeField = field.GetCustomAttribute(SerializeFieldType) != null; + var isGeneratedByCompiler = field.GetCustomAttribute(CompilerGeneratedType) != null; + var canBeSerialized = (hasSerializeField || !field.IsNotSerialized) && !isGeneratedByCompiler; + if (!canBeSerialized) continue; + + if (!objNode.TryGetValue(field.Name, out var rawValue)) continue; + + var converted = ConvertToType(rawValue, field.FieldType); + field.SetValue(instance, converted); + } + + // If the target uses Unity callbacks, respect them (parity with existing Serializer behavior) + if (instance is ISerializationCallbackReceiver receiver) + { + receiver.OnAfterDeserialize(); + } + + return instance; + } + + throw new InvalidOperationException( + $"SmallerJSON cannot convert node type [{node.GetType().FullName}] to [{targetType.FullName}]."); + } + + private static bool TryGetSemanticPrimitiveType(Type semanticType, out Type primitiveType) + { + var iface = semanticType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition().FullName == BeamSemanticInterfaceFullName); + + if (iface == null) + { + primitiveType = null; + return false; + } + + primitiveType = iface.GetGenericArguments()[0]; + return true; + } + + private static object ConstructSemantic(Type semanticType, Type primitiveType, object node) + { + // Prefer ctor(string) when JSON is a string. Many long-backed semantic types accept string too + // and preserve formatting (leading zeros etc) or validate (ServiceName). + if (node is string s) + { + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor != null) return stringCtor.Invoke(new object[] { s }); + + if (primitiveType == typeof(long)) + { + if (!long.TryParse(s, System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, out var parsed)) + { + throw new FormatException($"Cannot parse '{s}' as long for semantic type [{semanticType.FullName}]."); + } + + var longCtor = semanticType.GetConstructor(new[] { typeof(long) }); + if (longCtor != null) return longCtor.Invoke(new object[] { parsed }); + } + + throw new MissingMethodException( + $"Semantic type [{semanticType.FullName}] has no public ctor(string) and cannot be built from JSON string."); + } + + // If JSON is numeric and semantic primitive is long + if (primitiveType == typeof(long)) + { + var l = Convert.ToInt64(node, System.Globalization.CultureInfo.InvariantCulture); + var longCtor = semanticType.GetConstructor(new[] { typeof(long) }); + if (longCtor != null) return longCtor.Invoke(new object[] { l }); + + // Some semantic types might only expose ctor(string); allow numeric -> string as fallback. + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor != null) return stringCtor.Invoke(new object[] { l.ToString(System.Globalization.CultureInfo.InvariantCulture) }); + + throw new MissingMethodException($"Semantic type [{semanticType.FullName}] must have ctor(long) or ctor(string)."); + } + + // If semantic primitive is string, coerce to string + if (primitiveType == typeof(string)) + { + var stringCtor = semanticType.GetConstructor(new[] { typeof(string) }); + if (stringCtor == null) + throw new MissingMethodException($"Semantic type [{semanticType.FullName}] must have ctor(string)."); + + return stringCtor.Invoke(new object[] { node.ToString() }); + } + + throw new NotSupportedException( + $"Semantic type [{semanticType.FullName}] uses unsupported primitive [{primitiveType.FullName}]."); + } + } + } } diff --git a/client/Packages/com.beamable/Runtime/Environment/Resources/versions-default.json b/client/Packages/com.beamable/Runtime/Environment/Resources/versions-default.json index 8fec252ca8..9652e19964 100644 --- a/client/Packages/com.beamable/Runtime/Environment/Resources/versions-default.json +++ b/client/Packages/com.beamable/Runtime/Environment/Resources/versions-default.json @@ -1,3 +1,3 @@ { - "nugetPackageVersion": "0.0.0-PREVIEW.NIGHTLY-202512081843" + "nugetPackageVersion": "0.0.123" } \ No newline at end of file diff --git a/client/Packages/com.beamable/Runtime/Server/MicroserviceClient.cs b/client/Packages/com.beamable/Runtime/Server/MicroserviceClient.cs index 9fd06f6450..91c6da685e 100644 --- a/client/Packages/com.beamable/Runtime/Server/MicroserviceClient.cs +++ b/client/Packages/com.beamable/Runtime/Server/MicroserviceClient.cs @@ -3,12 +3,15 @@ using Beamable.Common.Api; using Beamable.Common.Api.Inventory; using Beamable.Common.Dependencies; +using Beamable.Common.Semantics; using Beamable.Serialization.SmallerJSON; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; +using System.Linq; +using System.Reflection; using System.Text; using UnityEngine; using Debug = UnityEngine.Debug; @@ -274,8 +277,8 @@ public static T DeserializeResult(string json) } return wrapped.items; } - - return JsonUtility.FromJson(json); + + return Json.Deserialize(json); } public static Dictionary ConvertArrayDictToDictionary(ArrayDict arrayDict) diff --git a/microservice/beamable.tooling.common/Microservice/ServiceMethodHelper.cs b/microservice/beamable.tooling.common/Microservice/ServiceMethodHelper.cs index 354d307024..f8144edb3b 100644 --- a/microservice/beamable.tooling.common/Microservice/ServiceMethodHelper.cs +++ b/microservice/beamable.tooling.common/Microservice/ServiceMethodHelper.cs @@ -8,7 +8,6 @@ using System.Text; using Beamable.Common.Dependencies; -using beamable.tooling.common.Microservice; using ZLogger; using static Beamable.Common.Constants.Features.Services; @@ -310,8 +309,13 @@ public static object DeserializeStringParameter(string json) return Serialization.SmallerJSON.Json.Serialize(dict, new StringBuilder()); } - // or just peel off the quotes - return json.Substring(1, json.Length - 2); + // or just peel off the quotes if it starts of ends with them + if (json.StartsWith("\"") && json.EndsWith("\"")) + { + return json.Substring(1, json.Length - 2); + } + // if not just throw back the original string + return json; } private static List ScanType(IMicroserviceAttributes serviceAttribute, ServiceMethodProvider provider) diff --git a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs index fb89053c26..dcfb72829a 100644 --- a/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/SchemaGenerator.cs @@ -39,7 +39,9 @@ public OAPIType(ServiceMethod method, Type type) public bool IsFromBeamGenerateSchema() => SourceCallable == null && Type.GetCustomAttribute() != null; public bool IsFromCallableWithNoClientGen() => IsFromCallable() && SourceCallable.Method.GetCustomAttribute(true).Flags.HasFlag(CallableFlags.SkipGenerateClientFiles); - public bool IsBeamSemanticType() => Type.GetCustomAttribute() != null; + + public bool IsBeamSemanticType() => Type.GetInterfaces() + .Any(i => i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)); public bool ShouldSkipClientCodeGeneration() => (IsFromFederation() || IsFromCallableWithNoClientGen() || IsBeamSemanticType()) && !IsFromBeamGenerateSchema(); @@ -209,8 +211,14 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti return Convert(x.GetGenericArguments()[0], depth - 1); case { } x when x.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)): var semanticType = x.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)); - - return Convert(semanticType.GetGenericArguments()[0], depth - 1); + var semanticTypeSchema = Convert(semanticType.GetGenericArguments()[0], depth - 1); + if (Activator.CreateInstance(runtimeType) is IBeamSemanticType semanticTypeInstance) + { + semanticTypeSchema.Extensions[SCHEMA_SEMANTIC_TYPE_NAME_KEY] = + new OpenApiString(semanticTypeInstance.SemanticName); + } + + return semanticTypeSchema; case { } x when x == typeof(double): return new OpenApiSchema { Type = "number", Format = "double" }; case { } x when x == typeof(float): @@ -320,22 +328,15 @@ public static OpenApiSchema Convert(Type runtimeType, int depth = 1, bool saniti var comment = DocsLoader.GetMemberComments(member); fieldSchema.Description = comment?.Summary; - // First check if the type has the BeamSemanticTypeAttribute (all IBeamSemanticType have it already) - var classBeamSemanticTypeAttr = member.FieldType.GetCustomAttribute(); - if (classBeamSemanticTypeAttr != null) - { - fieldSchema.Extensions[SCHEMA_SEMANTIC_TYPE_NAME_KEY] = - new OpenApiString(classBeamSemanticTypeAttr.SemanticType); - } - - // Then check if the method declaration has it, if it has, it will override the one from the class - var beamSemanticTypeAttribute = member.GetCustomAttribute(); - if (beamSemanticTypeAttribute != null) + + Type classSemanticType = member.FieldType.GetInterfaces().FirstOrDefault(i => + i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBeamSemanticType<>)); + if (classSemanticType != null && Activator.CreateInstance(member.FieldType) is IBeamSemanticType memberSemanticTypeInstance) { fieldSchema.Extensions[SCHEMA_SEMANTIC_TYPE_NAME_KEY] = - new OpenApiString(beamSemanticTypeAttribute.SemanticType); + new OpenApiString(memberSemanticTypeInstance.SemanticName); } - + schema.Properties[name] = fieldSchema; if (!member.FieldType.IsAssignableTo(typeof(Optional))) diff --git a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs index 3e086c77fe..c0ddc0b309 100644 --- a/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs +++ b/microservice/beamable.tooling.common/OpenAPI/ServiceDocGenerator.cs @@ -12,6 +12,7 @@ using Microsoft.OpenApi.Readers; using System.Reflection; using Beamable.Common.Dependencies; +using Beamable.Common.Semantics; using ZLogger; namespace Beamable.Tooling.Common.OpenAPI; @@ -235,12 +236,6 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r OpenApiSchema openApiSchema = SchemaGenerator.Convert(returnType, 0); - var beamSemanticType = method.Method.ReturnParameter.GetCustomAttribute(); - if (beamSemanticType != null) - { - openApiSchema.Extensions.Add(SCHEMA_SEMANTIC_TYPE_NAME_KEY, new OpenApiString(beamSemanticType.SemanticType)); - } - var returnJson = new OpenApiMediaType { Schema = openApiSchema }; if (openApiSchema.Reference != null && !doc.Components.Schemas.ContainsKey(openApiSchema.Reference.Id)) { @@ -289,12 +284,6 @@ public OpenApiDocument Generate(StartupContext startupCtx, IDependencyProvider r bool isOptional = parameterType.IsAssignableTo(typeof(Optional)); parameterSchema.Nullable = isNullable; parameterSchema.Extensions.Add(SCHEMA_IS_OPTIONAL_KEY, new OpenApiBoolean(isOptional)); - - var paramBeamSemanticType = method.ParameterInfos[i].GetCustomAttribute(); - if (paramBeamSemanticType != null) - { - parameterSchema.Extensions.Add(SCHEMA_SEMANTIC_TYPE_NAME_KEY, new OpenApiString(paramBeamSemanticType.SemanticType)); - } switch (parameterSource) { diff --git a/microservice/beamable.tooling.common/UnityJsonContractResolver.cs b/microservice/beamable.tooling.common/UnityJsonContractResolver.cs index cbc6ee684e..cdb8a0e539 100644 --- a/microservice/beamable.tooling.common/UnityJsonContractResolver.cs +++ b/microservice/beamable.tooling.common/UnityJsonContractResolver.cs @@ -1,8 +1,8 @@ using Beamable.Common; using Beamable.Common.Api.Inventory; using Beamable.Common.Content; +using Beamable.Common.Semantics; using System.Reflection; -using Beamable.Common.Semantics.JsonConverters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; @@ -30,10 +30,15 @@ public class UnitySerializationSettings : JsonSerializerSettings new StringToSomethingDictionaryConverter(), new StringToSomethingDictionaryConverter(), new StringToSomethingDictionaryConverter>(), - new OptionalConverter(), new BeamAccountIdConverter(), new BeamCidConverter(), + new BeamContentIdConverter(), + new BeamContentManifestIdConverter(), new BeamGamerTagConverter(), + new BeamPidConverter(), + new BeamStatsConverter(), + new ServiceNameConverter(), + new OptionalConverter(), // THIS MUST BE LAST, because it is hacky, and falls back onto other converts as its _normal_ behaviour. If its not last, then other converts can run twice, which causes newtonsoft to explode. new UnitySerializationCallbackInvoker(), @@ -291,6 +296,266 @@ public void Dispose() _skip.Dispose(); } } + + + /// + /// Custom JSON converter for to serialize as long and deserialize it from string and long + /// + public class BeamAccountIdConverter : JsonConverter + { + + public override BeamAccountId ReadJson(JsonReader reader, Type objectType, BeamAccountId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamAccountId(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamAccountId(stringValue); + } + case JsonToken.Null: + { + return new BeamAccountId(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamAccountId"); + } + } + + public override void WriteJson(JsonWriter writer, BeamAccountId value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + + + } + + /// + /// Custom JSON converter for to serialize as long and deserialize it from string and long + /// + public class BeamCidConverter : JsonConverter + { + public override BeamCid ReadJson(JsonReader reader, Type objectType, BeamCid existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamCid(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamCid(stringValue); + } + case JsonToken.Null: + { + return new BeamCid(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamCid value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class BeamContentIdConverter : JsonConverter + { + public override BeamContentId ReadJson(JsonReader reader, Type objectType, BeamContentId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamContentId(stringValue); + } + case JsonToken.Null: + { + return new BeamContentId(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamContentId value, JsonSerializer serializer) + { + writer.WriteValue(value.AsString); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class BeamContentManifestIdConverter : JsonConverter + { + public override BeamContentManifestId ReadJson(JsonReader reader, Type objectType, BeamContentManifestId existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamContentManifestId(stringValue); + } + case JsonToken.Null: + { + return new BeamContentManifestId(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamContentManifestId value, JsonSerializer serializer) + { + writer.WriteValue(value.AsString); + } + } + + + /// + /// Custom JSON converter for to serialize as long and deserialize it from string and long + /// + public class BeamGamerTagConverter : JsonConverter + { + public override BeamGamerTag ReadJson(JsonReader reader, Type objectType, BeamGamerTag existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Integer: + { + var longValue = Convert.ToInt64(reader.Value); + return new BeamGamerTag(longValue); + } + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamGamerTag(stringValue); + } + case JsonToken.Null: + { + return new BeamGamerTag(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamGamerTag value, JsonSerializer serializer) + { + writer.WriteValue(value.AsLong); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class BeamPidConverter : JsonConverter + { + public override BeamPid ReadJson(JsonReader reader, Type objectType, BeamPid existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamPid(stringValue); + } + case JsonToken.Null: + { + return new BeamPid(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamPid value, JsonSerializer serializer) + { + writer.WriteValue(value.AsString); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class BeamStatsConverter : JsonConverter + { + public override BeamStats ReadJson(JsonReader reader, Type objectType, BeamStats existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new BeamStats(stringValue); + } + case JsonToken.Null: + { + return new BeamStats(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, BeamStats value, JsonSerializer serializer) + { + writer.WriteValue(value.AsString); + } + } + + /// + /// Custom JSON converter for to serialize and deserialize it from string + /// + public class ServiceNameConverter : JsonConverter + { + public override ServiceName ReadJson(JsonReader reader, Type objectType, ServiceName existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + { + var stringValue = (string)reader.Value; + return new ServiceName(stringValue); + } + case JsonToken.Null: + { + return new ServiceName(); + } + default: + throw new JsonSerializationException($"Unexpected token type {reader.TokenType} when parsing BeamCid"); + } + } + + public override void WriteJson(JsonWriter writer, ServiceName value, JsonSerializer serializer) + { + writer.WriteValue(value.Value); + } + } /// /// Custom contract resolver for JSON serialization in Unity, allowing for proper handling of SerializeField attributes. From 66517d816032319c677fde01b1d4317f2b26fcf4 Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Mon, 15 Dec 2025 10:01:50 -0300 Subject: [PATCH 3/9] change: Added support to Equatable Long, string and struct to each SemanticType --- .../Runtime/Semantics/BeamAccountId.cs | 22 +++++++++++++++++- .../Runtime/Semantics/BeamCid.cs | 23 +++++++++++++++++-- .../Runtime/Semantics/BeamContentId.cs | 17 +++++++++++++- .../Semantics/BeamContentManifestId.cs | 17 +++++++++++++- .../Runtime/Semantics/BeamGamerTag.cs | 22 +++++++++++++++++- .../Runtime/Semantics/BeamPid.cs | 17 +++++++++++++- .../Runtime/Semantics/BeamStats.cs | 17 +++++++++++++- .../Runtime/Semantics/ServiceName.cs | 12 +++++++++- .../Common/Runtime/Semantics/BeamAccountId.cs | 22 +++++++++++++++++- .../Common/Runtime/Semantics/BeamCid.cs | 23 +++++++++++++++++-- .../Common/Runtime/Semantics/BeamContentId.cs | 17 +++++++++++++- .../Semantics/BeamContentManifestId.cs | 17 +++++++++++++- .../Common/Runtime/Semantics/BeamGamerTag.cs | 22 +++++++++++++++++- .../Common/Runtime/Semantics/BeamPid.cs | 17 +++++++++++++- .../Common/Runtime/Semantics/BeamStats.cs | 17 +++++++++++++- 15 files changed, 265 insertions(+), 17 deletions(-) diff --git a/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs b/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs index 1ef7a9f4b1..a9eb934051 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamAccountId.cs @@ -4,7 +4,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamAccountId : IBeamSemanticType + public struct BeamAccountId : IBeamSemanticType, IEquatable, IEquatable, IEquatable { private long _longValue; private string _stringValue; @@ -64,5 +64,25 @@ public string ToJson() { return AsString; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamAccountId other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamCid.cs b/cli/beamable.common/Runtime/Semantics/BeamCid.cs index 95e73eb12a..a9a1f3e99d 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamCid.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamCid.cs @@ -1,11 +1,10 @@ using System; using Beamable.Common.BeamCli; -using Beamable.Serialization.SmallerJSON; namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamCid : IBeamSemanticType + public struct BeamCid : IBeamSemanticType, IEquatable, IEquatable, IEquatable { private long _longValue; private string _stringValue; @@ -63,5 +62,25 @@ public string ToJson() { return AsString; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamCid other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamContentId.cs b/cli/beamable.common/Runtime/Semantics/BeamContentId.cs index 5acacc0dde..46d62b8f14 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamContentId.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamContentId.cs @@ -5,7 +5,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamContentId : IBeamSemanticType + public struct BeamContentId : IBeamSemanticType, IEquatable, IEquatable { private string _value; @@ -33,5 +33,20 @@ public string ToJson() { return $"\"{AsString}\""; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamContentId other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs b/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs index 5b3200bcc9..a5b06a8c42 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamContentManifestId.cs @@ -4,7 +4,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamContentManifestId : IBeamSemanticType + public struct BeamContentManifestId : IBeamSemanticType, IEquatable, IEquatable { private string _value; @@ -27,5 +27,20 @@ public string ToJson() { return $"\"{AsString}\""; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamContentManifestId other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs b/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs index 647691d056..87e9b66402 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamGamerTag.cs @@ -4,7 +4,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamGamerTag : IBeamSemanticType + public struct BeamGamerTag : IBeamSemanticType, IEquatable, IEquatable, IEquatable { private long _longValue; private string _stringValue; @@ -63,5 +63,25 @@ public string ToJson() { return AsString; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamGamerTag other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamPid.cs b/cli/beamable.common/Runtime/Semantics/BeamPid.cs index 6007db2fe9..57c6fef4df 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamPid.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamPid.cs @@ -4,7 +4,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamPid : IBeamSemanticType + public struct BeamPid : IBeamSemanticType, IEquatable, IEquatable { private string _stringValue; @@ -27,5 +27,20 @@ public string ToJson() { return $"\"{AsString}\""; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamPid other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/BeamStats.cs b/cli/beamable.common/Runtime/Semantics/BeamStats.cs index 25feae1322..fa007d2208 100644 --- a/cli/beamable.common/Runtime/Semantics/BeamStats.cs +++ b/cli/beamable.common/Runtime/Semantics/BeamStats.cs @@ -5,7 +5,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamStats : IBeamSemanticType + public struct BeamStats : IBeamSemanticType, IEquatable, IEquatable { private string _value; @@ -40,5 +40,20 @@ public string ToJson() { return $"\"{AsString}\""; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamStats other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/cli/beamable.common/Runtime/Semantics/ServiceName.cs b/cli/beamable.common/Runtime/Semantics/ServiceName.cs index c8f9af5bb8..5e6b27ffe6 100644 --- a/cli/beamable.common/Runtime/Semantics/ServiceName.cs +++ b/cli/beamable.common/Runtime/Semantics/ServiceName.cs @@ -5,7 +5,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct ServiceName : IBeamSemanticType + public struct ServiceName : IBeamSemanticType, IEquatable, IEquatable { public string Value { get; } @@ -32,6 +32,16 @@ public ServiceName(string value) public static implicit operator string(ServiceName d) => d.Value; public static implicit operator ServiceName(string s) => new ServiceName(s); + public bool Equals(string other) + { + return other == Value; + } + + public bool Equals(ServiceName other) + { + return other.Value == Value; + } + public override string ToString() { return Value; diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs index 969f40f297..f63a647cb5 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamAccountId.cs @@ -7,7 +7,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamAccountId : IBeamSemanticType + public struct BeamAccountId : IBeamSemanticType, IEquatable, IEquatable, IEquatable { private long _longValue; private string _stringValue; @@ -67,5 +67,25 @@ public string ToJson() { return AsString; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamAccountId other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs index 53e53ae424..6dbf8aa59e 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamCid.cs @@ -3,12 +3,11 @@ using System; using Beamable.Common.BeamCli; -using Beamable.Serialization.SmallerJSON; namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamCid : IBeamSemanticType + public struct BeamCid : IBeamSemanticType, IEquatable, IEquatable, IEquatable { private long _longValue; private string _stringValue; @@ -66,5 +65,25 @@ public string ToJson() { return AsString; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamCid other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs index 71718c1579..4bfcbef9f4 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentId.cs @@ -8,7 +8,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamContentId : IBeamSemanticType + public struct BeamContentId : IBeamSemanticType, IEquatable, IEquatable { private string _value; @@ -36,5 +36,20 @@ public string ToJson() { return $"\"{AsString}\""; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamContentId other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs index 956845d5d3..f81ecc3121 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamContentManifestId.cs @@ -7,7 +7,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamContentManifestId : IBeamSemanticType + public struct BeamContentManifestId : IBeamSemanticType, IEquatable, IEquatable { private string _value; @@ -30,5 +30,20 @@ public string ToJson() { return $"\"{AsString}\""; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamContentManifestId other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs index 647e424e95..b3325e9201 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamGamerTag.cs @@ -7,7 +7,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamGamerTag : IBeamSemanticType + public struct BeamGamerTag : IBeamSemanticType, IEquatable, IEquatable, IEquatable { private long _longValue; private string _stringValue; @@ -66,5 +66,25 @@ public string ToJson() { return AsString; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(long other) + { + return other == AsLong; + } + + public bool Equals(BeamGamerTag other) + { + return other.AsLong == AsLong; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs index 4792d17901..4a849af8d6 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamPid.cs @@ -7,7 +7,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamPid : IBeamSemanticType + public struct BeamPid : IBeamSemanticType, IEquatable, IEquatable { private string _stringValue; @@ -30,5 +30,20 @@ public string ToJson() { return $"\"{AsString}\""; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamPid other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } } } diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs index 44a02cb715..783030e472 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/BeamStats.cs @@ -8,7 +8,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct BeamStats : IBeamSemanticType + public struct BeamStats : IBeamSemanticType, IEquatable, IEquatable { private string _value; @@ -43,5 +43,20 @@ public string ToJson() { return $"\"{AsString}\""; } + + public bool Equals(string other) + { + return other == AsString; + } + + public bool Equals(BeamStats other) + { + return other.AsString == AsString; + } + + public override string ToString() + { + return AsString; + } } } From 4e3bd5055410d8aa334afb0489cadf52d2959f14 Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Tue, 16 Dec 2025 14:31:21 -0300 Subject: [PATCH 4/9] change: Commit autogen files --- .../Common/Runtime/Semantics/ServiceName.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs b/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs index 45441462d2..c18ceb102d 100644 --- a/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs +++ b/client/Packages/com.beamable/Common/Runtime/Semantics/ServiceName.cs @@ -8,7 +8,7 @@ namespace Beamable.Common.Semantics { [CliContractType, Serializable] - public struct ServiceName : IBeamSemanticType + public struct ServiceName : IBeamSemanticType, IEquatable, IEquatable { public string Value { get; } @@ -35,6 +35,16 @@ public ServiceName(string value) public static implicit operator string(ServiceName d) => d.Value; public static implicit operator ServiceName(string s) => new ServiceName(s); + public bool Equals(string other) + { + return other == Value; + } + + public bool Equals(ServiceName other) + { + return other.Value == Value; + } + public override string ToString() { return Value; From ee1c49f6ff664ef19a26cfc803133042f7736efd Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Tue, 16 Dec 2025 15:13:49 -0300 Subject: [PATCH 5/9] change: merge conflict solve --- cli/beamable.common/beamable.common.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/beamable.common/beamable.common.csproj b/cli/beamable.common/beamable.common.csproj index 705274cdcd..025891f28b 100644 --- a/cli/beamable.common/beamable.common.csproj +++ b/cli/beamable.common/beamable.common.csproj @@ -55,10 +55,10 @@ - + Date: Mon, 5 Jan 2026 15:31:31 -0300 Subject: [PATCH 6/9] new: Commands to manage replacement types --- cli/cli/App.cs | 4 + .../Project/AddReplacementTypeCommand.cs | 139 ++++++++++++++++++ .../Project/GenerateClientFileCommand.cs | 96 ++++++------ .../Project/ListReplacementTypeCommand.cs | 65 ++++++++ .../Project/RemoveReplacementTypeCommand.cs | 92 ++++++++++++ cli/cli/Services/ConfigService.cs | 16 ++ .../BeamListReplacementTypeCommandOutput.cs | 12 ++ ...amListReplacementTypeCommandOutput.cs.meta | 11 ++ .../Commands/BeamProjectAddReplacementType.cs | 81 ++++++++++ .../BeamProjectAddReplacementType.cs.meta | 11 ++ .../BeamProjectListReplacementType.cs | 58 ++++++++ .../BeamProjectListReplacementType.cs.meta | 11 ++ .../BeamProjectRemoveReplacementType.cs | 60 ++++++++ .../BeamProjectRemoveReplacementType.cs.meta | 11 ++ .../Commands/BeamReplacementTypeInfo.cs | 15 ++ .../Commands/BeamReplacementTypeInfo.cs.meta | 11 ++ 16 files changed, 650 insertions(+), 43 deletions(-) create mode 100644 cli/cli/Commands/Project/AddReplacementTypeCommand.cs create mode 100644 cli/cli/Commands/Project/ListReplacementTypeCommand.cs create mode 100644 cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs.meta create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs create mode 100644 client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs.meta diff --git a/cli/cli/App.cs b/cli/cli/App.cs index 7c9b9dabb3..feb5637c33 100644 --- a/cli/cli/App.cs +++ b/cli/cli/App.cs @@ -594,6 +594,10 @@ public virtual void Configure( Commands.AddSubCommand(); Commands.AddSubCommand(); + Commands.AddSubCommand(); + Commands.AddSubCommand(); + Commands.AddSubCommand(); + Commands.AddSubCommand(); Commands.AddRootCommand(); diff --git a/cli/cli/Commands/Project/AddReplacementTypeCommand.cs b/cli/cli/Commands/Project/AddReplacementTypeCommand.cs new file mode 100644 index 0000000000..d3610d0595 --- /dev/null +++ b/cli/cli/Commands/Project/AddReplacementTypeCommand.cs @@ -0,0 +1,139 @@ +using System.CommandLine; +using cli.Services; +using cli.Utils; +using Markdig.Extensions.TaskLists; +using Spectre.Console; + +namespace cli.Commands.Project; + + +public class AddReplacementTypeCommandArgs : CommandArgs +{ + public string ReferenceId; + public string EngineReplacementType; + public string EngineOptionalReplacementType; + public string EngineImport; + public string UnrealProjectName; +} +public class AddReplacementTypeCommand : AppCommand, IEmptyResult +{ + public AddReplacementTypeCommand() : base("add-replacement-type", "Add a replacement type for all Unreal linked projects") + { + } + + public override void Configure() + { + AddOption(new Option("reference-id", "The reference Id (C# class/struct name) for the replacement"), (args, i) => args.ReferenceId = i); + AddOption(new Option("replacement-type", "The name of the Type to replaced with in Unreal auto-gen"), (args, i) => args.EngineReplacementType = i); + AddOption(new Option("optional-replacement-type", "The name of the Optional Type to replaced with in Unreal auto-gen"), (args, i) => args.EngineOptionalReplacementType = i); + AddOption(new Option("engine-import", "The full import for the replacement type to be used in Unreal auto-gen"), (args, i) => args.EngineImport = i); + AddOption(new Option("project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + } + + public override async Task Handle(AddReplacementTypeCommandArgs args) + { + var replacementType = new ReplacementTypeInfo() + { + ReferenceId = await GetReferenceId(args), + EngineReplacementType = await GetEngineReplacementType(args), + EngineOptionalReplacementType = await GetEngineOptionalReplacementType(args), + EngineImport = await GetEngineImport(args) + }; + + var linkedEngineProjects = args.ConfigService.GetLinkedEngineProjects(); + var unrealProjects = linkedEngineProjects.unrealProjectsPaths; + + if (unrealProjects.Count == 0) + { + throw new CliException("No Unreal project linked, please link a project first with `beam project add-unreal-project `"); + } + + // Check if it has specific project name + if (!string.IsNullOrEmpty(args.UnrealProjectName)) + { + if (unrealProjects.Any(item => item.GetProjectName() == args.UnrealProjectName)) + { + var projectData = unrealProjects.FirstOrDefault(item => item.GetProjectName() == args.UnrealProjectName); + linkedEngineProjects.unrealProjectsPaths.Remove(projectData); + projectData.ReplacementTypeInfos = projectData.ReplacementTypeInfos.Append(replacementType).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(projectData); + args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + } + else + { + throw new CliException($"Project {args.UnrealProjectName} not found"); + } + return; + } + + // If not, we need to check which options we have + if (unrealProjects.Count == 1 || args.Quiet) + { + var onlyItem = unrealProjects.First(); + linkedEngineProjects.unrealProjectsPaths.Remove(onlyItem); + onlyItem.ReplacementTypeInfos = onlyItem.ReplacementTypeInfos.Append(replacementType).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(onlyItem); + args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + return; + } + + string projectSelection = GetProjectPrompt(unrealProjects); + + var selectedProject = unrealProjects.First(item => item.GetProjectName() == projectSelection); + linkedEngineProjects.unrealProjectsPaths.Remove(selectedProject); + selectedProject.ReplacementTypeInfos = selectedProject.ReplacementTypeInfos.Append(replacementType).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(selectedProject); + args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + } + + private static string GetProjectPrompt(HashSet unrealProjects) + { + var projectSelection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Which [green]project[/] do you want to add the Replacement Type?") + .AddChoices(unrealProjects.Select(item => item.GetProjectName())) + .AddBeamHightlight() + ); + return projectSelection; + } + + private Task GetReferenceId(AddReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.ReferenceId)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the Replacement [green]Reference Id[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.ReferenceId); + } + + private Task GetEngineReplacementType(AddReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.EngineReplacementType)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the [green]Engine Replacement Type[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.EngineReplacementType); + } + + private Task GetEngineOptionalReplacementType(AddReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.EngineOptionalReplacementType)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the [green]Engine Optional Replacement Type[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.EngineOptionalReplacementType); + } + + private Task GetEngineImport(AddReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.EngineImport)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the [green]Engine Import[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.EngineImport); + } +} diff --git a/cli/cli/Commands/Project/GenerateClientFileCommand.cs b/cli/cli/Commands/Project/GenerateClientFileCommand.cs index 2439af2cd0..89f022af85 100644 --- a/cli/cli/Commands/Project/GenerateClientFileCommand.cs +++ b/cli/cli/Commands/Project/GenerateClientFileCommand.cs @@ -43,6 +43,50 @@ public class GenerateClientFileCommand : AppCommand , IResultSteam { + private static readonly Dictionary BaseReplacementTypeInfos = new() + { + { + "ClientPermission", new ReplacementTypeInfo + { + ReferenceId = "ClientPermission", + EngineReplacementType = "FBeamClientPermission", + EngineOptionalReplacementType = + $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamClientPermission", + EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamClientPermission.h""", + } + }, + { + "ExternalIdentity", new ReplacementTypeInfo + { + ReferenceId = "ExternalIdentity", + EngineReplacementType = "FBeamExternalIdentity", + EngineOptionalReplacementType = + $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamExternalIdentity", + EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamExternalIdentity.h""", + } + }, + { + "Tag", new ReplacementTypeInfo + { + ReferenceId = "Tag", + EngineReplacementType = "FBeamTag", + EngineOptionalReplacementType = $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamTag", + EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamTag.h""", + } + }, + { + "ClientContentInfoJson", new ReplacementTypeInfo() + { + ReferenceId = "ClientContentInfoJson", + EngineReplacementType = "FBeamRemoteContentManifestEntry", + EngineOptionalReplacementType = + $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamRemoteContentManifestEntry", + EngineImport = + @"#include ""BeamBackend/ReplacementTypes/BeamRemoteContentManifestEntry.h""", + } + } + }; + public override bool IsForInternalUse => true; public GenerateClientFileCommand() : base("generate-client", @@ -313,53 +357,19 @@ public override async Task Handle(GenerateClientFileCommandArgs args) UnrealSourceGenerator.previousGenerationPassesData = JsonConvert.DeserializeObject(File.ReadAllText(previousGenerationFilePath)); UnrealSourceGenerator.currentGenerationPassDataFilePath = $"{unrealProjectData.CoreProjectName}_GenerationPass"; + + var replacementTypes = new Dictionary(BaseReplacementTypeInfos); + + foreach (ReplacementTypeInfo replacementTypeInfo in unrealProjectData.ReplacementTypeInfos) + { + replacementTypes.TryAdd(replacementTypeInfo.ReferenceId, replacementTypeInfo); + } + var unrealFileDescriptors = unrealGenerator.Generate(new SwaggerService.DefaultGenerationContext { Documents = docs, OrderedSchemas = orderedSchemas, - ReplacementTypes = new Dictionary - { - { - "ClientPermission", new ReplacementTypeInfo - { - ReferenceId = "ClientPermission", - EngineReplacementType = "FBeamClientPermission", - EngineOptionalReplacementType = - $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamClientPermission", - EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamClientPermission.h""", - } - }, - { - "ExternalIdentity", new ReplacementTypeInfo - { - ReferenceId = "ExternalIdentity", - EngineReplacementType = "FBeamExternalIdentity", - EngineOptionalReplacementType = - $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamExternalIdentity", - EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamExternalIdentity.h""", - } - }, - { - "Tag", new ReplacementTypeInfo - { - ReferenceId = "Tag", - EngineReplacementType = "FBeamTag", - EngineOptionalReplacementType = $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamTag", - EngineImport = @"#include ""BeamBackend/ReplacementTypes/BeamTag.h""", - } - }, - { - "ClientContentInfoJson", new ReplacementTypeInfo() - { - ReferenceId = "ClientContentInfoJson", - EngineReplacementType = "FBeamRemoteContentManifestEntry", - EngineOptionalReplacementType = - $"{UnrealSourceGenerator.UNREAL_OPTIONAL}BeamRemoteContentManifestEntry", - EngineImport = - @"#include ""BeamBackend/ReplacementTypes/BeamRemoteContentManifestEntry.h""", - } - } - } + ReplacementTypes = replacementTypes, }); Log.Verbose($"completed in-memory generation of clients for project {unrealProjectData.CoreProjectName} path=[{unrealProjectData.Path}], total ms {sw.ElapsedMilliseconds}"); diff --git a/cli/cli/Commands/Project/ListReplacementTypeCommand.cs b/cli/cli/Commands/Project/ListReplacementTypeCommand.cs new file mode 100644 index 0000000000..5a04ac5cc8 --- /dev/null +++ b/cli/cli/Commands/Project/ListReplacementTypeCommand.cs @@ -0,0 +1,65 @@ +using System.CommandLine; +using cli.Services; +using cli.Utils; +using Markdig.Extensions.TaskLists; +using Spectre.Console; + +namespace cli.Commands.Project; + + +public class ListReplacementTypeCommandArgs : CommandArgs +{ + public string UnrealProjectName; +} + +public class ListReplacementTypeCommandOutput +{ + public List ReplacementsTypes; +} +public class ListReplacementTypeCommand : AtomicCommand +{ + public ListReplacementTypeCommand() : base("list-replacement-type", "Add a replacement type for all Unreal linked projects") + { + } + + public override void Configure() + { + AddOption(new Option("project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + } + + public override async Task GetResult(ListReplacementTypeCommandArgs args) + { + var linkedEngineProjects = args.ConfigService.GetLinkedEngineProjects(); + var unrealProjects = linkedEngineProjects.unrealProjectsPaths; + + if (unrealProjects.Count == 0) + { + throw new CliException("No Unreal project linked, please link a project first with `beam project add-unreal-project `"); + } + if (unrealProjects.Count == 1 || args.Quiet) + { + var onlyItem = unrealProjects.First(); + return new ListReplacementTypeCommandOutput { ReplacementsTypes = onlyItem.ReplacementTypeInfos.ToList() }; + } + + var projectName = GetProjectName(args, unrealProjects); + var selectedProject = unrealProjects.First(item => item.GetProjectName() == projectName); + return new ListReplacementTypeCommandOutput { ReplacementsTypes = selectedProject.ReplacementTypeInfos.ToList() }; + } + + private static string GetProjectName(ListReplacementTypeCommandArgs args, HashSet unrealProjects) + { + if (!string.IsNullOrEmpty(args.UnrealProjectName)) + { + return args.UnrealProjectName; + } + + string projectSelection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Which [green]project[/] do you want to add the Replacement Type?") + .AddChoices(unrealProjects.Select(item => item.GetProjectName())) + .AddBeamHightlight() + ); + return projectSelection; + } +} diff --git a/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs b/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs new file mode 100644 index 0000000000..cd4694e657 --- /dev/null +++ b/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs @@ -0,0 +1,92 @@ +using System.CommandLine; +using cli.Services; +using cli.Utils; +using Markdig.Extensions.TaskLists; +using Spectre.Console; + +namespace cli.Commands.Project; + + +public class RemoveReplacementTypeCommandArgs : CommandArgs +{ + public string ReferenceId; + public string UnrealProjectName; +} +public class RemoveReplacementTypeCommand : AppCommand, IEmptyResult +{ + public RemoveReplacementTypeCommand() : base("remove-replacement-type", "Add a replacement type for all Unreal linked projects") + { + } + + public override void Configure() + { + AddOption(new Option("reference-id", "The reference Id (C# class/struct name) for the replacement"), (args, i) => args.ReferenceId = i); + AddOption(new Option("project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + } + + public override async Task Handle(RemoveReplacementTypeCommandArgs args) + { + + var referenceId = await GetReferenceId(args); + + var linkedEngineProjects = args.ConfigService.GetLinkedEngineProjects(); + var unrealProjects = linkedEngineProjects.unrealProjectsPaths; + + if (unrealProjects.Count == 0) + { + throw new CliException("No Unreal project linked, please link a project first with `beam project add-unreal-project `"); + } + + // Check if it has specific project name + if (!string.IsNullOrEmpty(args.UnrealProjectName)) + { + if (unrealProjects.Any(item => item.GetProjectName() == args.UnrealProjectName)) + { + var projectData = unrealProjects.FirstOrDefault(item => item.GetProjectName() == args.UnrealProjectName); + linkedEngineProjects.unrealProjectsPaths.Remove(projectData); + projectData.ReplacementTypeInfos = projectData.ReplacementTypeInfos.Where(item => item.ReferenceId != referenceId).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(projectData); + args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + } + else + { + throw new CliException($"Project {args.UnrealProjectName} not found"); + } + return; + } + + // If not, we need to check which options we have + if (unrealProjects.Count == 1 || args.Quiet) + { + var onlyItem = unrealProjects.First(); + linkedEngineProjects.unrealProjectsPaths.Remove(onlyItem); + onlyItem.ReplacementTypeInfos = onlyItem.ReplacementTypeInfos.Where(item => item.ReferenceId != referenceId).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(onlyItem); + args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + return; + } + + var projectSelection = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Which [green]project[/] do you want to add the Replacement Type?") + .AddChoices(unrealProjects.Select(item => item.GetProjectName())) + .AddBeamHightlight() + ); + + var selectedProject = unrealProjects.First(item => item.GetProjectName() == projectSelection); + linkedEngineProjects.unrealProjectsPaths.Remove(selectedProject); + selectedProject.ReplacementTypeInfos = selectedProject.ReplacementTypeInfos.Where(item => item.ReferenceId != referenceId).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(selectedProject); + args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + } + + private Task GetReferenceId(RemoveReplacementTypeCommandArgs args) + { + if (string.IsNullOrEmpty(args.ReferenceId)) + { + return Task.FromResult(AnsiConsole.Prompt( + new TextPrompt("Please enter the Replacement [green]Reference Id[/]:").PromptStyle("green"))); + } + return Task.FromResult(args.ReferenceId); + } +} diff --git a/cli/cli/Services/ConfigService.cs b/cli/cli/Services/ConfigService.cs index 3b90a42744..01544ce200 100644 --- a/cli/cli/Services/ConfigService.cs +++ b/cli/cli/Services/ConfigService.cs @@ -1560,6 +1560,10 @@ public override bool Equals(object obj) => (obj is Unreal unity && Equals(unity) [Serializable] public struct Unreal : IEquatable, IEquatable { + + public const string CORE_NAME_SUFFIX = "MicroserviceClients"; + public const string BP_CORE_NAME_SUFFIX = "MicroserviceClientsBp"; + /// /// Name for the project's core module (the module every other module has access to). /// This will be used to generate the ______API UE Macros for the generated types. @@ -1609,6 +1613,13 @@ public struct Unreal : IEquatable, IEquatable /// public string BeamableBackendGenerationPassFile; + public ReplacementTypeInfo[] ReplacementTypeInfos; + + public string GetProjectName() + { + return CoreProjectName.Remove(CoreProjectName.Length - CORE_NAME_SUFFIX.Length); + } + public bool Equals(string other) => Path.Equals(other); public bool Equals(Unreal other) => Path == other.Path; @@ -1619,5 +1630,10 @@ public override bool Equals(object obj) => (obj is Unreal unreal && Equals(unrea public static bool operator ==(Unreal left, Unreal right) => left.Equals(right); public static bool operator !=(Unreal left, Unreal right) => !(left == right); + + public static string GetCoreName(string projectName) => $"{projectName}{CORE_NAME_SUFFIX}"; + public static string GetBlueprintNodesProjectName(string projectName) => $"{projectName}{BP_CORE_NAME_SUFFIX}"; + + } } diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs new file mode 100644 index 0000000000..be6ec29cd6 --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs @@ -0,0 +1,12 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + [System.SerializableAttribute()] + public partial class BeamListReplacementTypeCommandOutput + { + public System.Collections.Generic.List ReplacementsTypes; + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs.meta new file mode 100644 index 0000000000..5551283cfb --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamListReplacementTypeCommandOutput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 10aa27a2bd2c15d7f1bf5b9d6db3c1ac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs new file mode 100644 index 0000000000..d5b907c1e4 --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs @@ -0,0 +1,81 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + public partial class ProjectAddReplacementTypeArgs : Beamable.Common.BeamCli.IBeamCommandArgs + { + /// The reference Id (C# class/struct name) for the replacement + public string referenceId; + /// The name of the Type to replaced with in Unreal auto-gen + public string replacementType; + /// The name of the Optional Type to replaced with in Unreal auto-gen + public string optionalReplacementType; + /// The full import for the replacement type to be used in Unreal auto-gen + public string engineImport; + /// The Unreal project name + public string projectName; + /// Serializes the arguments for command line usage. + public virtual string Serialize() + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + // If the referenceId value was not default, then add it to the list of args. + if ((this.referenceId != default(string))) + { + genBeamCommandArgs.Add(("--reference-id=" + this.referenceId)); + } + // If the replacementType value was not default, then add it to the list of args. + if ((this.replacementType != default(string))) + { + genBeamCommandArgs.Add(("--replacement-type=" + this.replacementType)); + } + // If the optionalReplacementType value was not default, then add it to the list of args. + if ((this.optionalReplacementType != default(string))) + { + genBeamCommandArgs.Add(("--optional-replacement-type=" + this.optionalReplacementType)); + } + // If the engineImport value was not default, then add it to the list of args. + if ((this.engineImport != default(string))) + { + genBeamCommandArgs.Add(("--engine-import=" + this.engineImport)); + } + // If the projectName value was not default, then add it to the list of args. + if ((this.projectName != default(string))) + { + genBeamCommandArgs.Add(("--project-name=" + this.projectName)); + } + string genBeamCommandStr = ""; + // Join all the args with spaces + genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + return genBeamCommandStr; + } + } + public partial class BeamCommands + { + public virtual ProjectAddReplacementTypeWrapper ProjectAddReplacementType(ProjectAddReplacementTypeArgs addReplacementTypeArgs) + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + genBeamCommandArgs.Add("beam"); + genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); + genBeamCommandArgs.Add("project"); + genBeamCommandArgs.Add("add-replacement-type"); + genBeamCommandArgs.Add(addReplacementTypeArgs.Serialize()); + // Create an instance of an IBeamCommand + Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); + // Join all the command paths and args into one string + string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + // Configure the command with the command string + command.SetCommand(genBeamCommandStr); + ProjectAddReplacementTypeWrapper genBeamCommandWrapper = new ProjectAddReplacementTypeWrapper(); + genBeamCommandWrapper.Command = command; + // Return the command! + return genBeamCommandWrapper; + } + } + public partial class ProjectAddReplacementTypeWrapper : Beamable.Common.BeamCli.BeamCommandWrapper + { + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs.meta new file mode 100644 index 0000000000..fc0bfd3590 --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectAddReplacementType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53ecf17ad873c9afb1a32a2a2d1bea03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs new file mode 100644 index 0000000000..dd9ea9120d --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs @@ -0,0 +1,58 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + public partial class ProjectListReplacementTypeArgs : Beamable.Common.BeamCli.IBeamCommandArgs + { + /// The Unreal project name + public string projectName; + /// Serializes the arguments for command line usage. + public virtual string Serialize() + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + // If the projectName value was not default, then add it to the list of args. + if ((this.projectName != default(string))) + { + genBeamCommandArgs.Add(("--project-name=" + this.projectName)); + } + string genBeamCommandStr = ""; + // Join all the args with spaces + genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + return genBeamCommandStr; + } + } + public partial class BeamCommands + { + public virtual ProjectListReplacementTypeWrapper ProjectListReplacementType(ProjectListReplacementTypeArgs listReplacementTypeArgs) + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + genBeamCommandArgs.Add("beam"); + genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); + genBeamCommandArgs.Add("project"); + genBeamCommandArgs.Add("list-replacement-type"); + genBeamCommandArgs.Add(listReplacementTypeArgs.Serialize()); + // Create an instance of an IBeamCommand + Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); + // Join all the command paths and args into one string + string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + // Configure the command with the command string + command.SetCommand(genBeamCommandStr); + ProjectListReplacementTypeWrapper genBeamCommandWrapper = new ProjectListReplacementTypeWrapper(); + genBeamCommandWrapper.Command = command; + // Return the command! + return genBeamCommandWrapper; + } + } + public partial class ProjectListReplacementTypeWrapper : Beamable.Common.BeamCli.BeamCommandWrapper + { + public virtual ProjectListReplacementTypeWrapper OnStreamListReplacementTypeCommandOutput(System.Action> cb) + { + this.Command.On("stream", cb); + return this; + } + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs.meta new file mode 100644 index 0000000000..dca4323a0e --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectListReplacementType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e37ef1f024919fd96ed29153ef07b580 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs new file mode 100644 index 0000000000..9cef525cce --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs @@ -0,0 +1,60 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + public partial class ProjectRemoveReplacementTypeArgs : Beamable.Common.BeamCli.IBeamCommandArgs + { + /// The reference Id (C# class/struct name) for the replacement + public string referenceId; + /// The Unreal project name + public string projectName; + /// Serializes the arguments for command line usage. + public virtual string Serialize() + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + // If the referenceId value was not default, then add it to the list of args. + if ((this.referenceId != default(string))) + { + genBeamCommandArgs.Add(("--reference-id=" + this.referenceId)); + } + // If the projectName value was not default, then add it to the list of args. + if ((this.projectName != default(string))) + { + genBeamCommandArgs.Add(("--project-name=" + this.projectName)); + } + string genBeamCommandStr = ""; + // Join all the args with spaces + genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + return genBeamCommandStr; + } + } + public partial class BeamCommands + { + public virtual ProjectRemoveReplacementTypeWrapper ProjectRemoveReplacementType(ProjectRemoveReplacementTypeArgs removeReplacementTypeArgs) + { + // Create a list of arguments for the command + System.Collections.Generic.List genBeamCommandArgs = new System.Collections.Generic.List(); + genBeamCommandArgs.Add("beam"); + genBeamCommandArgs.Add(defaultBeamArgs.Serialize()); + genBeamCommandArgs.Add("project"); + genBeamCommandArgs.Add("remove-replacement-type"); + genBeamCommandArgs.Add(removeReplacementTypeArgs.Serialize()); + // Create an instance of an IBeamCommand + Beamable.Common.BeamCli.IBeamCommand command = this._factory.Create(); + // Join all the command paths and args into one string + string genBeamCommandStr = string.Join(" ", genBeamCommandArgs); + // Configure the command with the command string + command.SetCommand(genBeamCommandStr); + ProjectRemoveReplacementTypeWrapper genBeamCommandWrapper = new ProjectRemoveReplacementTypeWrapper(); + genBeamCommandWrapper.Command = command; + // Return the command! + return genBeamCommandWrapper; + } + } + public partial class ProjectRemoveReplacementTypeWrapper : Beamable.Common.BeamCli.BeamCommandWrapper + { + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs.meta new file mode 100644 index 0000000000..4ff624fe8d --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamProjectRemoveReplacementType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d9a99992c11188eb60e8887c4c17631 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs new file mode 100644 index 0000000000..002743914b --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs @@ -0,0 +1,15 @@ + +namespace Beamable.Editor.BeamCli.Commands +{ + using Beamable.Common; + using Beamable.Common.BeamCli; + + [System.SerializableAttribute()] + public partial class BeamReplacementTypeInfo + { + public string ReferenceId; + public string EngineReplacementType; + public string EngineOptionalReplacementType; + public string EngineImport; + } +} diff --git a/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs.meta b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs.meta new file mode 100644 index 0000000000..2ef99b86e6 --- /dev/null +++ b/client/Packages/com.beamable/Editor/BeamCli/Commands/BeamReplacementTypeInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7ebfca2c0f0f8a4cf406e9257590d78 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From b6acf133c7dbe46341e11494193c8918ebede4ff Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Wed, 7 Jan 2026 10:31:35 -0300 Subject: [PATCH 7/9] fix: Issue when adding or removing replacement types when the field is null --- .../Project/AddReplacementTypeCommand.cs | 23 ++++++++++--------- .../Project/RemoveReplacementTypeCommand.cs | 23 ++++++++++--------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/cli/cli/Commands/Project/AddReplacementTypeCommand.cs b/cli/cli/Commands/Project/AddReplacementTypeCommand.cs index d3610d0595..843622bde8 100644 --- a/cli/cli/Commands/Project/AddReplacementTypeCommand.cs +++ b/cli/cli/Commands/Project/AddReplacementTypeCommand.cs @@ -54,10 +54,7 @@ public override async Task Handle(AddReplacementTypeCommandArgs args) if (unrealProjects.Any(item => item.GetProjectName() == args.UnrealProjectName)) { var projectData = unrealProjects.FirstOrDefault(item => item.GetProjectName() == args.UnrealProjectName); - linkedEngineProjects.unrealProjectsPaths.Remove(projectData); - projectData.ReplacementTypeInfos = projectData.ReplacementTypeInfos.Append(replacementType).ToArray(); - linkedEngineProjects.unrealProjectsPaths.Add(projectData); - args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + AddReplacementAndSetLinkedEngineProjects(args, linkedEngineProjects, projectData, replacementType); } else { @@ -70,19 +67,23 @@ public override async Task Handle(AddReplacementTypeCommandArgs args) if (unrealProjects.Count == 1 || args.Quiet) { var onlyItem = unrealProjects.First(); - linkedEngineProjects.unrealProjectsPaths.Remove(onlyItem); - onlyItem.ReplacementTypeInfos = onlyItem.ReplacementTypeInfos.Append(replacementType).ToArray(); - linkedEngineProjects.unrealProjectsPaths.Add(onlyItem); - args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + AddReplacementAndSetLinkedEngineProjects(args, linkedEngineProjects, onlyItem, replacementType); return; } string projectSelection = GetProjectPrompt(unrealProjects); var selectedProject = unrealProjects.First(item => item.GetProjectName() == projectSelection); - linkedEngineProjects.unrealProjectsPaths.Remove(selectedProject); - selectedProject.ReplacementTypeInfos = selectedProject.ReplacementTypeInfos.Append(replacementType).ToArray(); - linkedEngineProjects.unrealProjectsPaths.Add(selectedProject); + AddReplacementAndSetLinkedEngineProjects(args, linkedEngineProjects, selectedProject, replacementType); + } + + private static void AddReplacementAndSetLinkedEngineProjects(AddReplacementTypeCommandArgs args, + EngineProjectData linkedEngineProjects, EngineProjectData.Unreal onlyItem, ReplacementTypeInfo replacementType) + { + var array = onlyItem.ReplacementTypeInfos ?? Array.Empty(); + linkedEngineProjects.unrealProjectsPaths.Remove(onlyItem); + onlyItem.ReplacementTypeInfos = array.Append(replacementType).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(onlyItem); args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); } diff --git a/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs b/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs index cd4694e657..b7c9530d8c 100644 --- a/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs +++ b/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs @@ -43,10 +43,7 @@ public override async Task Handle(RemoveReplacementTypeCommandArgs args) if (unrealProjects.Any(item => item.GetProjectName() == args.UnrealProjectName)) { var projectData = unrealProjects.FirstOrDefault(item => item.GetProjectName() == args.UnrealProjectName); - linkedEngineProjects.unrealProjectsPaths.Remove(projectData); - projectData.ReplacementTypeInfos = projectData.ReplacementTypeInfos.Where(item => item.ReferenceId != referenceId).ToArray(); - linkedEngineProjects.unrealProjectsPaths.Add(projectData); - args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + RemoveReferenceAndSetLinkedEngineProjects(args, linkedEngineProjects, projectData, referenceId); } else { @@ -59,10 +56,7 @@ public override async Task Handle(RemoveReplacementTypeCommandArgs args) if (unrealProjects.Count == 1 || args.Quiet) { var onlyItem = unrealProjects.First(); - linkedEngineProjects.unrealProjectsPaths.Remove(onlyItem); - onlyItem.ReplacementTypeInfos = onlyItem.ReplacementTypeInfos.Where(item => item.ReferenceId != referenceId).ToArray(); - linkedEngineProjects.unrealProjectsPaths.Add(onlyItem); - args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + RemoveReferenceAndSetLinkedEngineProjects(args, linkedEngineProjects, onlyItem, referenceId); return; } @@ -74,9 +68,16 @@ public override async Task Handle(RemoveReplacementTypeCommandArgs args) ); var selectedProject = unrealProjects.First(item => item.GetProjectName() == projectSelection); - linkedEngineProjects.unrealProjectsPaths.Remove(selectedProject); - selectedProject.ReplacementTypeInfos = selectedProject.ReplacementTypeInfos.Where(item => item.ReferenceId != referenceId).ToArray(); - linkedEngineProjects.unrealProjectsPaths.Add(selectedProject); + RemoveReferenceAndSetLinkedEngineProjects(args, linkedEngineProjects, selectedProject, referenceId); + } + + private static void RemoveReferenceAndSetLinkedEngineProjects(RemoveReplacementTypeCommandArgs args, + EngineProjectData linkedEngineProjects, EngineProjectData.Unreal projectData, string referenceId) + { + var array = projectData.ReplacementTypeInfos ?? Array.Empty(); + linkedEngineProjects.unrealProjectsPaths.Remove(projectData); + projectData.ReplacementTypeInfos = array.Where(item => item.ReferenceId != referenceId).ToArray(); + linkedEngineProjects.unrealProjectsPaths.Add(projectData); args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); } From aaa28ae63b960326a5afdd32a6af9dfc31c17df9 Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Wed, 7 Jan 2026 11:33:14 -0300 Subject: [PATCH 8/9] change: Updated CHANGELOG.md --- cli/cli/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/cli/CHANGELOG.md b/cli/cli/CHANGELOG.md index 0b5808558c..f19d328736 100644 --- a/cli/cli/CHANGELOG.md +++ b/cli/cli/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - `net10` support +- New Commands `project add-replacement-type`, `project list-replacement-type`, `project remove-replacement-type` to manage Unreal replacement types. +- Semantic Types for Beamable Classes with custom serialization and deserialization ### Fixed - possible `IndexOutOfBounds` error when running `beam project ps` due to nameless docker containers From 92b3851154b969568ecc0e44fce595a3f8ca7384 Mon Sep 17 00:00:00 2001 From: Gabriel Moraes Date: Wed, 7 Jan 2026 16:06:25 -0300 Subject: [PATCH 9/9] new: Added support to CustomReplacementTypes module on Unreal. --- .../Commands/Project/AddReplacementTypeCommand.cs | 13 +++++++------ .../Commands/Project/GenerateClientFileCommand.cs | 15 ++++++++++++++- .../Project/ListReplacementTypeCommand.cs | 2 +- .../Project/RemoveReplacementTypeCommand.cs | 4 ++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/cli/cli/Commands/Project/AddReplacementTypeCommand.cs b/cli/cli/Commands/Project/AddReplacementTypeCommand.cs index 843622bde8..daebc046cb 100644 --- a/cli/cli/Commands/Project/AddReplacementTypeCommand.cs +++ b/cli/cli/Commands/Project/AddReplacementTypeCommand.cs @@ -23,11 +23,11 @@ public class AddReplacementTypeCommand : AppCommand("reference-id", "The reference Id (C# class/struct name) for the replacement"), (args, i) => args.ReferenceId = i); - AddOption(new Option("replacement-type", "The name of the Type to replaced with in Unreal auto-gen"), (args, i) => args.EngineReplacementType = i); - AddOption(new Option("optional-replacement-type", "The name of the Optional Type to replaced with in Unreal auto-gen"), (args, i) => args.EngineOptionalReplacementType = i); - AddOption(new Option("engine-import", "The full import for the replacement type to be used in Unreal auto-gen"), (args, i) => args.EngineImport = i); - AddOption(new Option("project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + AddOption(new Option("--reference-id", "The reference Id (C# class/struct name) for the replacement"), (args, i) => args.ReferenceId = i); + AddOption(new Option("--replacement-type", "The name of the Type to replaced with in Unreal auto-gen"), (args, i) => args.EngineReplacementType = i); + AddOption(new Option("--optional-replacement-type", "The name of the Optional Type to replaced with in Unreal auto-gen"), (args, i) => args.EngineOptionalReplacementType = i); + AddOption(new Option("--engine-import", "The full import for the replacement type to be used in Unreal auto-gen"), (args, i) => args.EngineImport = i); + AddOption(new Option("--project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); } public override async Task Handle(AddReplacementTypeCommandArgs args) @@ -85,6 +85,7 @@ private static void AddReplacementAndSetLinkedEngineProjects(AddReplacementTypeC onlyItem.ReplacementTypeInfos = array.Append(replacementType).ToArray(); linkedEngineProjects.unrealProjectsPaths.Add(onlyItem); args.ConfigService.SetLinkedEngineProjects(linkedEngineProjects); + AnsiConsole.MarkupLine($"Added replacement type [green]{replacementType.ReferenceId}[/] for [green]{onlyItem.GetProjectName()}[/]. Make sure to add the Replacement Type Under your [green]Plugins/BeamableUnrealMicroserviceClients/Source/CustomReplacementTypes[/] folder"); } private static string GetProjectPrompt(HashSet unrealProjects) @@ -103,7 +104,7 @@ private Task GetReferenceId(AddReplacementTypeCommandArgs args) if (string.IsNullOrEmpty(args.ReferenceId)) { return Task.FromResult(AnsiConsole.Prompt( - new TextPrompt("Please enter the Replacement [green]Reference Id[/]:").PromptStyle("green"))); + new TextPrompt("Please enter the Replacement [green]Reference Id[/] (Use the same name as in the OpenApi Specs):").PromptStyle("green"))); } return Task.FromResult(args.ReferenceId); } diff --git a/cli/cli/Commands/Project/GenerateClientFileCommand.cs b/cli/cli/Commands/Project/GenerateClientFileCommand.cs index 89f022af85..2d48f5aeb3 100644 --- a/cli/cli/Commands/Project/GenerateClientFileCommand.cs +++ b/cli/cli/Commands/Project/GenerateClientFileCommand.cs @@ -307,6 +307,7 @@ public override async Task Handle(GenerateClientFileCommandArgs args) // Handle Unreal code-gen var hasUnrealLinkedProjects = args.outputToLinkedProjects && args.ProjectService.GetLinkedUnrealProjects().Count > 0; + const string customReplacementTypesFolderName = "CustomReplacementTypes"; if (hasUnrealLinkedProjects) { // Check if the microservice projects are built and that the OAPI exists. @@ -407,7 +408,12 @@ public override async Task Handle(GenerateClientFileCommandArgs args) ""Name"": ""{unrealProjectData.BlueprintNodesProjectName}"", ""Type"": ""UncookedOnly"", ""LoadingPhase"": ""Default"" - }} + }}, + {{ + ""Name"": ""{customReplacementTypesFolderName}"", + ""Type"": ""Runtime"", + ""LoadingPhase"": ""Default"" + }}, ], ""Plugins"": [ {{ @@ -710,6 +716,13 @@ class F{unrealProjectData.BlueprintNodesProjectName}Module : public IModuleInter } await Task.WhenAll(writeFiles); + + var replacementTypeFolder = new DirectoryInfo(Path.Join(outputDir, "Source", customReplacementTypesFolderName)); + if (!replacementTypeFolder.Exists) + { + Directory.CreateDirectory(replacementTypeFolder.FullName); + } + Log.Verbose($"completed writing auto-generated files to disk {unrealProjectData.CoreProjectName} path=[{unrealProjectData.Path}], total ms {sw.ElapsedMilliseconds}"); // Run the Regenerate Project Files utility for the project (so that create files are automatically updated in IDEs). diff --git a/cli/cli/Commands/Project/ListReplacementTypeCommand.cs b/cli/cli/Commands/Project/ListReplacementTypeCommand.cs index 5a04ac5cc8..15ef8266bf 100644 --- a/cli/cli/Commands/Project/ListReplacementTypeCommand.cs +++ b/cli/cli/Commands/Project/ListReplacementTypeCommand.cs @@ -24,7 +24,7 @@ public class ListReplacementTypeCommand : AtomicCommand("project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + AddOption(new Option("--project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); } public override async Task GetResult(ListReplacementTypeCommandArgs args) diff --git a/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs b/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs index b7c9530d8c..d591ae62d5 100644 --- a/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs +++ b/cli/cli/Commands/Project/RemoveReplacementTypeCommand.cs @@ -20,8 +20,8 @@ public class RemoveReplacementTypeCommand : AppCommand("reference-id", "The reference Id (C# class/struct name) for the replacement"), (args, i) => args.ReferenceId = i); - AddOption(new Option("project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); + AddOption(new Option("--reference-id", "The reference Id (C# class/struct name) for the replacement"), (args, i) => args.ReferenceId = i); + AddOption(new Option("--project-name", "The Unreal project name"), (args, i) => args.UnrealProjectName = i); } public override async Task Handle(RemoveReplacementTypeCommandArgs args)