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}");