using Prospect.Unreal.Core; using Prospect.Unreal.Core.Names; using Prospect.Unreal.Exceptions; using Prospect.Unreal.Net; using Prospect.Unreal.Net.Actors; using Prospect.Unreal.Net.Channels; using Prospect.Unreal.Net.Packets.Bunch; using Prospect.Unreal.Net.Packets.Control; using Serilog; namespace Prospect.Unreal.Runtime; public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable { private static readonly ILogger Logger = Log.ForContext(); private UGameInstance? _owningGameInstance; private AGameModeBase? _authorityGameMode; private List _levels; public UWorld() { _owningGameInstance = null; _authorityGameMode = null; _levels = new List(); } /// /// Persistent level containing the world info, default brush and actors spawned during gameplay among other things /// public ULevel? PersistentLevel { get; private set; } /// /// The NAME_GameNetDriver game connection(s) for client/server communication /// public UNetDriver? NetDriver { get; private set; } /// /// Whether actors have been initialized for play /// public bool bActorsInitialized { get; private set; } /// /// Whether BeginPlay has been called on actors /// public bool bBegunPlay { get; private set; } /// /// The URL that was used when loading this World. /// public FUrl? Url { get; private set; } /// /// Time in seconds since level began play, but IS paused when the game is paused, and IS dilated/clamped. /// public float TimeSeconds { get; private set; } /// /// Time in seconds since level began play, but IS NOT paused when the game is paused, and IS dilated/clamped. /// public float UnpausedTimeSeconds { get; private set; } /// /// Time in seconds since level began play, but IS NOT paused when the game is paused, and IS NOT dilated/clamped. /// public float RealTimeSeconds { get; private set; } /// /// Time in seconds since level began play, but IS paused when the game is paused, and IS NOT dilated/clamped. /// public float AudioTimeSeconds { get; private set; } /// /// Frame delta time in seconds adjusted by e.g. time dilation. /// public float DeltaTimeSeconds { get; private set; } public void Tick(float deltaTime) { if (NetDriver != null) { NetDriver.TickDispatch(deltaTime); NetDriver.PostTickDispatch(); NetDriver.TickFlush(deltaTime); NetDriver.PostTickFlush(); } } public void SetGameInstance(UGameInstance instance) { _owningGameInstance = instance; } public UGameInstance GetGameInstance() { if (_owningGameInstance == null) { throw new UnrealException($"Attempted to retrieve null {nameof(UGameInstance)}"); } return _owningGameInstance; } public bool SetGameMode(FUrl worldUrl) { if (IsServer() && _authorityGameMode == null) { _authorityGameMode = GetGameInstance().CreateGameModeForURL(worldUrl, this); if (_authorityGameMode != null) { return true; } Logger.Error("Failed to spawn GameMode actor"); return false; } return false; } public AGameModeBase? GetAuthGameMode() { return _authorityGameMode; } public void InitializeActorsForPlay(FUrl inUrl, bool bResetTime) { // Don't reset time for seamless world transitions. if (bResetTime) { TimeSeconds = 0.0f; UnpausedTimeSeconds = 0.0f; RealTimeSeconds = 0.0f; AudioTimeSeconds = 0.0f; } // Get URL Options var options = inUrl.OptionsToString(); // Set level info. if (!string.IsNullOrEmpty(inUrl.GetOption("load", null))) { Url = inUrl; } // Init level gameplay info. if (!AreActorsInitialized()) { // Initialize network actors and start execution. for (int i = 0; i < _levels.Count; i++) { _levels[i].InitializeNetworkActors(); } // Enable actor script calls. bStartup = true; bActorsInitialized = true; // Spawn server actors // TODO: GEngine SpawnServerActors. // Init the game mode. if (_authorityGameMode != null && !_authorityGameMode.IsActorInitialized()) { _authorityGameMode.InitGame(inUrl.Map /* TODO: FPaths.GetBaseFilename */, options, out _); } } } public bool Listen() { if (NetDriver != null) { Logger.Error("NetDriver already exists"); return false; } NetDriver = new UIpNetDriver(Url.Host, Url.Port, IsServer()); NetDriver.SetWorld(this); if (!((UIpNetDriver)NetDriver).InitListen(this)) { Logger.Error("Failed to listen"); NetDriver = null; return false; } return true; } public EAcceptConnection NotifyAcceptingConnection() { return EAcceptConnection.Accept; } public void NotifyAcceptedConnection(UNetConnection connection) { } public bool NotifyAcceptingChannel(UChannel channel) { if (channel.Connection?.Driver == null) { throw new UnrealNetException(); } var driver = channel.Connection.Driver; if (!driver.IsServer()) { throw new NotSupportedException("Client code"); } else { // We are the server. if (driver.ChannelDefinitionMap[channel.ChName].ClientOpen) { // The client has opened initial channel. Logger.Verbose("NotifyAcceptingChannel {ChName} {ChIndex} server {FullName}: Accepted", channel.ChName, channel.ChIndex, typeof(UWorld).FullName); return true; } // Client can't open any other kinds of channels. Logger.Verbose("NotifyAcceptingChannel {ChName} {ChIndex} server {FullName}: Refused", channel.ChName, channel.ChIndex, typeof(UWorld).FullName); return false; } } public void NotifyControlMessage(UNetConnection connection, NMT messageType, FInBunch bunch) { if (NetDriver == null) { throw new UnrealNetException(); } if (!NetDriver.IsServer()) { throw new NotSupportedException("Client code"); } else { Logger.Verbose("Level server received: {MessageType}", messageType); if (!connection.IsClientMsgTypeValid(messageType)) { Logger.Error("IsClientMsgTypeValid FAILED ({MessageType}): Remote Address = {Address}", (int)messageType, connection.LowLevelGetRemoteAddress()); bunch.SetError(); return; } switch (messageType) { case NMT.Hello: { const int localNetworkVersion = 0; if (NMT_Hello.Receive(bunch, out var isLittleEndian, out var remoteNetworkVersion, out var encryptionToken)) { Logger.Information("Client connecting with version. LocalNetworkVersion: {Local}, RemoteNetworkVersion: {Remote}", localNetworkVersion, remoteNetworkVersion); // TODO: Version check. if (string.IsNullOrEmpty(encryptionToken)) { connection.SendChallengeControlMessage(); } else { throw new NotImplementedException("Encryption"); } } break; } case NMT.Netspeed: { if (NMT_Netspeed.Receive(bunch, out var rate)) { connection.CurrentNetSpeed = Math.Clamp(rate, 1800, NetDriver.MaxClientRate); Logger.Debug("Client netspeed is {Num}", connection.CurrentNetSpeed); } break; } case NMT.Abort: { break; } case NMT.Skip: { break; } case NMT.Login: { // Admit or deny the player here. 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 = connection.RequestURL; var oneIndex = newRequestUrl.IndexOf('?'); var twoIndex = newRequestUrl.IndexOf('#'); if (oneIndex != -1 && twoIndex != -1) { newRequestUrl = newRequestUrl.Substring(Math.Min(oneIndex, twoIndex)); } else if (oneIndex != -1) { newRequestUrl = newRequestUrl.Substring(oneIndex); } else if (twoIndex != -1) { newRequestUrl = newRequestUrl.Substring(twoIndex); } else { newRequestUrl = string.Empty; } Logger.Debug("Login request: {RequestUrl} userId: {UserId} platform: {Platform}", newRequestUrl, uniqueIdRepl.ToDebugString(), onlinePlatformName); // Compromise for passing splitscreen playercount through to gameplay login code, // without adding a lot of extra unnecessary complexity throughout the login code. // NOTE: This code differs from NMT_JoinSplit, by counting + 1 for SplitscreenCount // (since this is the primary connection, not counted in Children) // TODO: Implement proper FUrl constructor var inUrl = new FUrl { Map = Url.Map + newRequestUrl }; if (!inUrl.Valid) { connection.RequestURL = newRequestUrl; Logger.Error("NMT_Login: Invalid URL {Url}", connection.RequestURL); bunch.SetError(); break; } var splitscreenCount = Math.Min(connection.Children.Count + 1, 255); // Don't allow clients to specify this value inUrl.Options.Remove("SplitscreenCount"); inUrl.Options.Add($"SplitscreenCount={splitscreenCount}"); connection.RequestURL = inUrl.ToString(); // skip to the first option in the URL var tmp = connection.RequestURL.Substring(connection.RequestURL.IndexOf('?')); // keep track of net id for player associated with remote connection connection.PlayerId = uniqueIdRepl; // keep track of the online platform the player associated with this connection is using. connection.SetPlayerOnlinePlatformName(new FName(onlinePlatformName)); // ask the game code if this player can join string? errorMsg = null; var gameMode = GetAuthGameMode(); if (gameMode != null) { gameMode.PreLogin(tmp, connection.LowLevelGetRemoteAddress(), connection.PlayerId, out errorMsg); } if (!string.IsNullOrEmpty(errorMsg)) { Logger.Debug("PreLogin failure: {Error}", errorMsg); NMT_Failure.Send(connection, errorMsg); connection.FlushNet(true); } else { WelcomePlayer(connection); } } else { connection.ClientResponse = string.Empty; } 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}"); } } } } private void WelcomePlayer(UNetConnection connection) { // TODO: Properly fetch level name from CurrentLevel var levelName = "/Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap"; // TODO: Properly fetch from AuthorityGameMode var gameName = "/Script/ThirdPersonMP.ThirdPersonMPGameMode"; var redirectUrl = string.Empty; NMT_Welcome.Send(connection, levelName, gameName, redirectUrl); connection.FlushNet(); // connection.QueuedBits = 0; connection.SetClientLoginState(EClientLoginState.Welcomed); } public bool IsServer() { if (NetDriver != null) { return NetDriver.IsServer(); } return true; } public bool HasBegunPlay() { return bBegunPlay && PersistentLevel != null && PersistentLevel.Actors.Count != 0; } public bool AreActorsInitialized() { return bActorsInitialized && PersistentLevel != null && PersistentLevel.Actors.Count != 0; } public async ValueTask DisposeAsync() { if (NetDriver != null) { await NetDriver.DisposeAsync(); } } }