diff --git a/src/Prospect.Unreal/Core/ENetRole.cs b/src/Prospect.Unreal/Core/ENetRole.cs new file mode 100644 index 0000000..f8a6380 --- /dev/null +++ b/src/Prospect.Unreal/Core/ENetRole.cs @@ -0,0 +1,14 @@ +namespace Prospect.Unreal.Core; + +public enum ENetRole +{ + /** No role at all. */ + ROLE_None, + /** Locally simulated proxy of this actor. */ + ROLE_SimulatedProxy, + /** Locally autonomous proxy of this actor. */ + ROLE_AutonomousProxy, + /** Authoritative control over the actor. */ + ROLE_Authority, + ROLE_MAX, +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Actors/AActor.cs b/src/Prospect.Unreal/Net/Actors/AActor.cs index 7897d12..95a00d6 100644 --- a/src/Prospect.Unreal/Net/Actors/AActor.cs +++ b/src/Prospect.Unreal/Net/Actors/AActor.cs @@ -1,6 +1,35 @@ -namespace Prospect.Unreal.Net.Actors; +using Prospect.Unreal.Core; + +namespace Prospect.Unreal.Net.Actors; public class AActor { - + /// + /// Sets the value of Role without causing other side effects to this instance. + /// + public void SetRole(ENetRole inRole) + { + // TODO: Implement + throw new NotImplementedException(); + } + + /// + /// Set whether this actor replicates to network clients. When this actor is spawned on the server it will be sent to clients as well. + /// Properties flagged for replication will update on clients if they change on the server. + /// Internally changes the RemoteRole property and handles the cases where the actor needs to be added to the network actor list. + /// + public void SetReplicates(bool bInReplicates) + { + // TODO: Implement + throw new NotImplementedException(); + } + + /// + /// Sets whether or not this Actor is an autonomous proxy, which is an actor on a network client that is controlled by a user on that client. + /// + public void SetAutonomousProxy(bool bInAutonomousProxy, bool bAllowForcePropertyCompare = true) + { + // TODO: Implement + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Actors/AController.cs b/src/Prospect.Unreal/Net/Actors/AController.cs new file mode 100644 index 0000000..3b87b28 --- /dev/null +++ b/src/Prospect.Unreal/Net/Actors/AController.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net.Actors; + +public class AController : AActor +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs b/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs index 16252d3..6b97101 100644 --- a/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs +++ b/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs @@ -1,4 +1,7 @@ -namespace Prospect.Unreal.Net.Actors; +using Prospect.Unreal.Core; +using Prospect.Unreal.Runtime; + +namespace Prospect.Unreal.Net.Actors; public class AGameModeBase : AInfo { @@ -7,4 +10,16 @@ public class AGameModeBase : AInfo // Login unique id must match server expected unique id type OR No unique id could mean game doesn't use them errorMessage = null; } + + public APlayerController? Login(UPlayer newPlayer, ENetRole inRemoteRole, string portal, string options, FUniqueNetIdRepl uniqueId, out string errorMessage) + { + errorMessage = string.Empty; + throw new NotImplementedException(); + return null; + } + + public void PostLogin(APlayerController newPlayer) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Actors/APlayerController.cs b/src/Prospect.Unreal/Net/Actors/APlayerController.cs index 2ca7ea2..a9feaca 100644 --- a/src/Prospect.Unreal/Net/Actors/APlayerController.cs +++ b/src/Prospect.Unreal/Net/Actors/APlayerController.cs @@ -1,6 +1,18 @@ -namespace Prospect.Unreal.Net.Player; +using Prospect.Unreal.Runtime; -public class APlayerController +namespace Prospect.Unreal.Net.Actors; + +public class APlayerController : AController { - + /// + /// Index identifying players using the same base connection (splitscreen clients) + /// Used by netcode to match replicated PlayerControllers to the correct splitscreen viewport and child connection + /// replicated via special internal code, not through normal variable replication + /// + public byte NetPlayerIndex { get; set; } + + public void SetPlayer(UPlayer inPlayer) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UNetConnection.cs b/src/Prospect.Unreal/Net/UNetConnection.cs index 590a78d..282d65f 100644 --- a/src/Prospect.Unreal/Net/UNetConnection.cs +++ b/src/Prospect.Unreal/Net/UNetConnection.cs @@ -8,7 +8,7 @@ using Prospect.Unreal.Net.Packets.Bunch; using Prospect.Unreal.Net.Packets.Control; using Prospect.Unreal.Net.Packets.Header; using Prospect.Unreal.Net.Packets.Header.Sequence; -using Prospect.Unreal.Net.Player; +using Prospect.Unreal.Runtime; using Prospect.Unreal.Serialization; using Serilog; @@ -240,6 +240,11 @@ public abstract class UNetConnection : UPlayer /// public string ClientResponse { get; set; } + /// + /// URL requested by client + /// + public string RequestURL { get; set; } + /// /// The last time an ack was received /// diff --git a/src/Prospect.Unreal/Runtime/UPlayer.cs b/src/Prospect.Unreal/Runtime/UPlayer.cs index bb8f971..bb97851 100644 --- a/src/Prospect.Unreal/Runtime/UPlayer.cs +++ b/src/Prospect.Unreal/Runtime/UPlayer.cs @@ -1,4 +1,6 @@ -namespace Prospect.Unreal.Net.Player; +using Prospect.Unreal.Net.Actors; + +namespace Prospect.Unreal.Runtime; public class UPlayer { diff --git a/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs b/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs new file mode 100644 index 0000000..5ee1db2 --- /dev/null +++ b/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs @@ -0,0 +1,53 @@ +using System.Text; +using Prospect.Unreal.Core; +using Prospect.Unreal.Net; +using Prospect.Unreal.Net.Actors; + +namespace Prospect.Unreal.Runtime; + +public partial class UWorld +{ + private APlayerController? SpawnPlayActor(UPlayer newPlayer, ENetRole remoteRole, FUrl inURL, FUniqueNetIdRepl uniqueId, out string error, byte inNetPlayerIndex = 0) + { + error = string.Empty; + + // Make the option string. + var optionsBuilder = new StringBuilder(); + + foreach (var op in inURL.Options) + { + optionsBuilder.Append('?'); + optionsBuilder.Append(op); + } + + var options = optionsBuilder.ToString(); + + var gameMode = GetAuthGameMode(); + if (gameMode != null) + { + var newPlayerController = gameMode.Login(newPlayer, remoteRole, inURL.Portal, options, uniqueId, out error); + if (newPlayerController == null) + { + Logger.Warning("Login failed: {Error}", error); + return null; + } + + // Logger.Debug("{A} got player {B} [{C}]", newPlayerController); + + // Possess the newly-spawned player. + newPlayerController.NetPlayerIndex = inNetPlayerIndex; + newPlayerController.SetRole(ENetRole.ROLE_Authority); + newPlayerController.SetReplicates(remoteRole != ENetRole.ROLE_None); + if (remoteRole == ENetRole.ROLE_AutonomousProxy) + { + newPlayerController.SetAutonomousProxy(true); + } + newPlayerController.SetPlayer(newPlayer); + gameMode.PostLogin(newPlayerController); + return newPlayerController; + } + + Logger.Warning("Login failed: No game mode set"); + return null; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Runtime/UWorld.cs b/src/Prospect.Unreal/Runtime/UWorld.cs index 7f79c22..55a9c33 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.cs @@ -1,3 +1,4 @@ +using System.Text; using Prospect.Unreal.Core; using Prospect.Unreal.Core.Names; using Prospect.Unreal.Exceptions; @@ -10,7 +11,7 @@ using Serilog; namespace Prospect.Unreal.Runtime; -public abstract class UWorld : FNetworkNotify, IAsyncDisposable +public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable { private static readonly ILogger Logger = Log.ForContext(); @@ -203,27 +204,28 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable case NMT.Login: { // Admit or deny the player here. - if (NMT_Login.Receive(bunch, out var clientResponse, out var requestUrl, out var uniqueIdRepl, out var onlinePlatformName)) + if (NMT_Login.Receive(bunch, out var clientResponse, out var tmpRequestUrl, out var uniqueIdRepl, out var onlinePlatformName)) { connection.ClientResponse = clientResponse; + connection.RequestURL = tmpRequestUrl; // Only the options/portal for the URL should be used during join - var newRequestUrl = requestUrl; + var newRequestUrl = connection.RequestURL; - var oneIndex = requestUrl.IndexOf('?'); - var twoIndex = requestUrl.IndexOf('#'); + var oneIndex = newRequestUrl.IndexOf('?'); + var twoIndex = newRequestUrl.IndexOf('#'); if (oneIndex != -1 && twoIndex != -1) { - newRequestUrl = requestUrl.Substring(Math.Min(oneIndex, twoIndex)); + newRequestUrl = newRequestUrl.Substring(Math.Min(oneIndex, twoIndex)); } else if (oneIndex != -1) { - newRequestUrl = requestUrl.Substring(oneIndex); + newRequestUrl = newRequestUrl.Substring(oneIndex); } else if (twoIndex != -1) { - newRequestUrl = requestUrl.Substring(twoIndex); + newRequestUrl = newRequestUrl.Substring(twoIndex); } else { @@ -244,8 +246,8 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable if (!inUrl.Valid) { - requestUrl = newRequestUrl; - Logger.Error("NMT_Login: Invalid URL {Url}", requestUrl); + connection.RequestURL = newRequestUrl; + Logger.Error("NMT_Login: Invalid URL {Url}", connection.RequestURL); bunch.SetError(); break; } @@ -256,10 +258,10 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable inUrl.Options.Remove("SplitscreenCount"); inUrl.Options.Add($"SplitscreenCount={splitscreenCount}"); - requestUrl = inUrl.ToString(); + connection.RequestURL = inUrl.ToString(); // skip to the first option in the URL - var tmp = requestUrl.Substring(requestUrl.IndexOf('?')); + var tmp = connection.RequestURL.Substring(connection.RequestURL.IndexOf('?')); // keep track of net id for player associated with remote connection connection.PlayerId = uniqueIdRepl; @@ -295,6 +297,21 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable break; } + case NMT.Join: + { + if (connection.PlayerController == null) + { + // Spawn the player-actor for this network player. + Logger.Debug("Join request: {Request}", connection.RequestURL); + + // TODO: Proper constructor + var inURL = new FUrl(); + + connection.PlayerController = SpawnPlayActor(connection, ENetRole.ROLE_AutonomousProxy, inURL, connection.PlayerId, out var errorMsg); + } + break; + } + default: { throw new NotImplementedException($"Unhandled control message {messageType}");