diff --git a/src/Prospect.Server.Game/Program.cs b/src/Prospect.Server.Game/Program.cs index a028e6a..811d2b6 100644 --- a/src/Prospect.Server.Game/Program.cs +++ b/src/Prospect.Server.Game/Program.cs @@ -36,11 +36,11 @@ internal static class Program Map = "/Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap" }; - //Task.Run(() => { Thread.Sleep(100); new Client().Connect("127.0.0.1", 7777, worldUrl); }); - await using (var world = new ProspectWorld(worldUrl)) + await using (var world = new ProspectWorld()) { world.SetGameInstance(new UGameInstance()); world.SetGameMode(worldUrl); + world.InitializeActorsForPlay(worldUrl, true); world.Listen(); while (await Tick.WaitForNextTickAsync()) diff --git a/src/Prospect.Server.Game/ProspectWorld.cs b/src/Prospect.Server.Game/ProspectWorld.cs index 2ff5b31..211f554 100644 --- a/src/Prospect.Server.Game/ProspectWorld.cs +++ b/src/Prospect.Server.Game/ProspectWorld.cs @@ -1,11 +1,7 @@ -using Prospect.Unreal.Core; -using Prospect.Unreal.Runtime; +using Prospect.Unreal.Runtime; namespace Prospect.Server.Game; public class ProspectWorld : UWorld { - public ProspectWorld(FUrl url) : base(url) - { - } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/EObjectFlags.cs b/src/Prospect.Unreal/Core/EObjectFlags.cs new file mode 100644 index 0000000..17258aa --- /dev/null +++ b/src/Prospect.Unreal/Core/EObjectFlags.cs @@ -0,0 +1,47 @@ +namespace Prospect.Unreal.Core; + +[Flags] +public enum EObjectFlags +{ + // Do not add new flags unless they truly belong here. There are alternatives. + // if you change any the bit of any of the RF_Load flags, then you will need legacy serialization + RF_NoFlags = 0x00000000, ///< No flags, used to avoid a cast + + // This first group of flags mostly has to do with what kind of object it is. Other than transient, these are the persistent object flags. + // The garbage collector also tends to look at these. + RF_Public =0x00000001, ///< Object is visible outside its package. + RF_Standalone =0x00000002, ///< Keep object around for editing even if unreferenced. + RF_MarkAsNative =0x00000004, ///< Object (UField) will be marked as native on construction (DO NOT USE THIS FLAG in HasAnyFlags() etc) + RF_Transactional =0x00000008, ///< Object is transactional. + RF_ClassDefaultObject =0x00000010, ///< This object is its class's default object + RF_ArchetypeObject =0x00000020, ///< This object is a template for another object - treat like a class default object + RF_Transient =0x00000040, ///< Don't save object. + + // This group of flags is primarily concerned with garbage collection. + RF_MarkAsRootSet =0x00000080, ///< Object will be marked as root set on construction and not be garbage collected, even if unreferenced (DO NOT USE THIS FLAG in HasAnyFlags() etc) + RF_TagGarbageTemp =0x00000100, ///< This is a temp user flag for various utilities that need to use the garbage collector. The garbage collector itself does not interpret it. + + // The group of flags tracks the stages of the lifetime of a uobject + RF_NeedInitialization =0x00000200, ///< This object has not completed its initialization process. Cleared when ~FObjectInitializer completes + RF_NeedLoad =0x00000400, ///< During load, indicates object needs loading. + RF_KeepForCooker =0x00000800, ///< Keep this object during garbage collection because it's still being used by the cooker + RF_NeedPostLoad =0x00001000, ///< Object needs to be postloaded. + RF_NeedPostLoadSubobjects =0x00002000, ///< During load, indicates that the object still needs to instance subobjects and fixup serialized component references + RF_NewerVersionExists =0x00004000, ///< Object has been consigned to oblivion due to its owner package being reloaded, and a newer version currently exists + RF_BeginDestroyed =0x00008000, ///< BeginDestroy has been called on the object. + RF_FinishDestroyed =0x00010000, ///< FinishDestroy has been called on the object. + + // Misc. Flags + RF_BeingRegenerated =0x00020000, ///< Flagged on UObjects that are used to create UClasses (e.g. Blueprints) while they are regenerating their UClass on load (See FLinkerLoad::CreateExport()), as well as UClass objects in the midst of being created + RF_DefaultSubObject =0x00040000, ///< Flagged on subobjects that are defaults + RF_WasLoaded =0x00080000, ///< Flagged on UObjects that were loaded + RF_TextExportTransient =0x00100000, ///< Do not export object to text form (e.g. copy/paste). Generally used for sub-objects that can be regenerated from data in their parent object. + RF_LoadCompleted =0x00200000, ///< Object has been completely serialized by linkerload at least once. DO NOT USE THIS FLAG, It should be replaced with RF_WasLoaded. + RF_InheritableComponentTemplate = 0x00400000, ///< Archetype of the object can be in its super class + RF_DuplicateTransient =0x00800000, ///< Object should not be included in any type of duplication (copy/paste, binary duplication, etc.) + RF_StrongRefOnFrame =0x01000000, ///< References to this object from persistent function frame are handled as strong ones. + RF_NonPIEDuplicateTransient =0x02000000, ///< Object should not be included for duplication unless it's being duplicated for a PIE session + RF_Dynamic =0x04000000, ///< Field Only. Dynamic field - doesn't get constructed during static initialization, can be constructed multiple times + RF_WillBeLoaded =0x08000000, ///< This object was constructed during load and will be loaded shortly + RF_HasExternalPackage =0x10000000, ///< This object has an external package assigned and should look it up when getting the outermost package +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/FUrl.cs b/src/Prospect.Unreal/Core/FUrl.cs index a2dd510..37166ce 100644 --- a/src/Prospect.Unreal/Core/FUrl.cs +++ b/src/Prospect.Unreal/Core/FUrl.cs @@ -18,6 +18,39 @@ public class FUrl public string Portal { get; set; } = string.Empty; public bool Valid { get; set; } = true; + public string? GetOption(string match, string? defaultValue) + { + var len = match.Length; + if (len > 0) + { + foreach (var option in Options) + { + if (option.StartsWith(match)) + { + if (option[len - 1] == '=' || option[len] == '=' || option.Length == len) + { + return option.Substring(len); + } + } + } + } + + return defaultValue; + } + + public string OptionsToString() + { + var optionsBuilder = new StringBuilder(); + + foreach (var op in Options) + { + optionsBuilder.Append('?'); + optionsBuilder.Append(op); + } + + return optionsBuilder.ToString(); + } + public string ToString(bool fullyQualified) { var result = new StringBuilder(); diff --git a/src/Prospect.Unreal/Core/ULevel.cs b/src/Prospect.Unreal/Core/ULevel.cs new file mode 100644 index 0000000..1eccac6 --- /dev/null +++ b/src/Prospect.Unreal/Core/ULevel.cs @@ -0,0 +1,26 @@ +using Prospect.Unreal.Net.Actors; + +namespace Prospect.Unreal.Core; + +public class ULevel +{ + public ULevel() + { + Actors = new List(); + } + + /// + /// URL associated with this level. + /// + public FUrl URL { get; private set; } + + /// + /// Array of all actors in this level, used by FActorIteratorBase and derived classes + /// + public List Actors { get; private set; } + + public void InitializeNetworkActors() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/UPackage.cs b/src/Prospect.Unreal/Core/UPackage.cs new file mode 100644 index 0000000..d7a1025 --- /dev/null +++ b/src/Prospect.Unreal/Core/UPackage.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Core; + +public class UPackage +{ + +} \ 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 95a00d6..aa05fb0 100644 --- a/src/Prospect.Unreal/Net/Actors/AActor.cs +++ b/src/Prospect.Unreal/Net/Actors/AActor.cs @@ -1,9 +1,12 @@ using Prospect.Unreal.Core; +using Prospect.Unreal.Runtime; namespace Prospect.Unreal.Net.Actors; public class AActor { + private bool bActorInitialized; + /// /// Sets the value of Role without causing other side effects to this instance. /// @@ -32,4 +35,14 @@ public class AActor // TODO: Implement throw new NotImplementedException(); } + + public UWorld GetWorld() + { + // if () + } + + public bool IsActorInitialized() + { + return bActorInitialized; + } } \ 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 6b97101..0e0b61b 100644 --- a/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs +++ b/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs @@ -5,6 +5,37 @@ namespace Prospect.Unreal.Net.Actors; public class AGameModeBase : AInfo { + public AGameModeBase() + { + OptionsString = string.Empty; + } + + /// + /// Save options string and parse it when needed + /// + public string OptionsString { get; set; } + + public AGameSession? GameSession { get; set; } + + public virtual void InitGame(string mapName, string options, out string errorMessage) + { + // Save Options for future use + OptionsString = options; + + var spawnInfo = new FActorSpawnParameters + { + Instigator = GetInstigator(), + ObjectFlags = EObjectFlags.RF_Transient + }; + + GameSession = World.SpawnActor(); + } + + public virtual void InitGameState() + { + throw new NotImplementedException(); + } + public void PreLogin(string options, string address, FUniqueNetIdRepl uniqueId, out string? errorMessage) { // Login unique id must match server expected unique id type OR No unique id could mean game doesn't use them @@ -14,8 +45,21 @@ public class AGameModeBase : AInfo public APlayerController? Login(UPlayer newPlayer, ENetRole inRemoteRole, string portal, string options, FUniqueNetIdRepl uniqueId, out string errorMessage) { errorMessage = string.Empty; + + if (GameSession == null) + { + errorMessage = "Failed to spawn player controller, GameSession is null"; + return null; + } + + errorMessage = GameSession.ApproveLogin(options); + + if (!string.IsNullOrEmpty(errorMessage)) + { + return null; + } + throw new NotImplementedException(); - return null; } public void PostLogin(APlayerController newPlayer) diff --git a/src/Prospect.Unreal/Net/Actors/AGameSession.cs b/src/Prospect.Unreal/Net/Actors/AGameSession.cs new file mode 100644 index 0000000..18eca33 --- /dev/null +++ b/src/Prospect.Unreal/Net/Actors/AGameSession.cs @@ -0,0 +1,12 @@ +namespace Prospect.Unreal.Net.Actors; + +public class AGameSession : AInfo +{ + public string ApproveLogin(string options) + { + // Check; + // - SpectatorOnly + // - SplitscreenCount + return string.Empty; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/ESpawnActorCollisionHandlingMethod.cs b/src/Prospect.Unreal/Net/ESpawnActorCollisionHandlingMethod.cs new file mode 100644 index 0000000..f95b7b6 --- /dev/null +++ b/src/Prospect.Unreal/Net/ESpawnActorCollisionHandlingMethod.cs @@ -0,0 +1,10 @@ +namespace Prospect.Unreal.Net; + +public enum ESpawnActorCollisionHandlingMethod +{ + Undefined, + AlwaysSpawn, + AdjustIfPossibleButAlwaysSpawn, + AdjustIfPossibleButDontSpawnIfColliding, + DontSpawnIfColliding +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/ESpawnActorNameMode.cs b/src/Prospect.Unreal/Net/ESpawnActorNameMode.cs new file mode 100644 index 0000000..e0c1dae --- /dev/null +++ b/src/Prospect.Unreal/Net/ESpawnActorNameMode.cs @@ -0,0 +1,24 @@ +namespace Prospect.Unreal.Net; + +public enum ESpawnActorNameMode +{ + /// + /// Fatal if unavailable, application will assert + /// + Required_Fatal, + + /// + /// Report an error return null if unavailable + /// + Required_ErrorAndReturnNull, + + /// + /// Return null if unavailable + /// + Required_ReturnNull, + + /// + /// If the supplied Name is already in use the generate an unused one using the supplied version as a base + /// + Requested +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FActorSpawnParameters.cs b/src/Prospect.Unreal/Net/FActorSpawnParameters.cs new file mode 100644 index 0000000..da3b9ae --- /dev/null +++ b/src/Prospect.Unreal/Net/FActorSpawnParameters.cs @@ -0,0 +1,105 @@ +using Prospect.Unreal.Core; +using Prospect.Unreal.Core.Names; +using Prospect.Unreal.Net.Actors; + +namespace Prospect.Unreal.Net; + +public ref struct FActorSpawnParameters +{ + /// + /// A name to assign as the Name of the Actor being spawned. + /// If no value is specified, the name of the spawned Actor will be automatically generated using the form [Class]_[Number]. + /// + public FName? Name { get; set; } + + /// + /// An Actor to use as a template when spawning the new Actor. + /// The spawned Actor will be initialized using the property values of the template Actor. + /// If left NULL the class default object (CDO) will be used to initialize the spawned Actor. + /// + public AActor? Template { get; set; } + + /// + /// The Actor that spawned this Actor. (Can be left as NULL). + /// + public AActor? Owner { get; set; } + + /// + /// The APawn that is responsible for damage done by the spawned Actor. (Can be left as NULL). + /// + public AActor? Instigator { get; set; } + + /// + /// The ULevel to spawn the Actor in, i.e. the Outer of the Actor. + /// If left as NULL the Outer of the Owner is used. If the Owner is NULL the persistent level is used. + /// + public ULevel? OverrideLevel { get; set; } + + /// + /// The UPackage to set the Actor in. + /// If left as NULL the Package will not be set and the actor will be saved in the same package as the persistent level. + /// + public UPackage? OverridePackage { get; set; } + + // UChildActorComponent + + /// + /// The Guid to set to this actor. Should only be set when reinstancing blueprint actors. + /// + public FGuid? OverrideActorGuid { get; set; } + + /// + /// Method for resolving collisions at the spawn point. Undefined means no override, use the actor's setting. + /// + public ESpawnActorCollisionHandlingMethod SpawnCollisionHandlingOverride { get; set; } + + /// + /// Is the actor remotely owned. + /// This should only be set true by the package map when it is creating an actor on a client that was replicated from the server. + /// + public bool bRemoteOwned { get; set; } + + /// + /// Determines whether spawning will not fail if certain conditions are not met. + /// If true, spawning will not fail because the class being spawned is `bStatic=true` or because the class of the template Actor is not the same as the class of the Actor being spawned. + /// + public bool bNoFail { get; set; } + + /// + /// Determines whether the construction script will be run. + /// If true, the construction script will not be run on the spawned Actor. + /// Only applicable if the Actor is being spawned from a Blueprint. + /// + public bool bDeferConstruction { get; set; } + + /// + /// Determines whether or not the actor may be spawned when running a construction script. + /// If true spawning will fail if a construction script is being run. + /// + public bool bAllowDuringConstructionScript { get; set; } + + /// + /// Determines whether the begin play cycle will run on the spawned actor when in the editor. + /// + public bool bTemporaryEditorActor { get; set; } + + /// + /// Determines whether or not the actor should be hidden from the Scene Outliner + /// + public bool bHideFromSceneOutliner { get; set; } + + /// + /// Determines whether to create a new package for the actor or not. + /// + public bool bCreateActorPackage { get; set; } + + /// + /// In which way should SpawnActor should treat the supplied Name if not none. + /// + public ESpawnActorNameMode NameMode { get; set; } + + /// + /// Flags used to describe the spawned actor/object instance. + /// + public EObjectFlags ObjectFlags { get; set; } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs b/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs index 5ee1db2..acf1774 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs @@ -12,15 +12,7 @@ public partial class UWorld 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 options = inURL.OptionsToString(); var gameMode = GetAuthGameMode(); if (gameMode != null) diff --git a/src/Prospect.Unreal/Runtime/UWorld.cs b/src/Prospect.Unreal/Runtime/UWorld.cs index 9a141a7..f64d913 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.cs @@ -1,4 +1,3 @@ -using System.Text; using Prospect.Unreal.Core; using Prospect.Unreal.Core.Names; using Prospect.Unreal.Exceptions; @@ -17,17 +16,65 @@ public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable private UGameInstance? _owningGameInstance; private AGameModeBase? _authorityGameMode; + private List _levels; - public UWorld(FUrl url) + public UWorld() { - Url = url; _owningGameInstance = null; _authorityGameMode = null; + _levels = new List(); } - public FUrl Url { get; } + /// + /// 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) @@ -77,7 +124,51 @@ public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable { 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) @@ -348,6 +439,16 @@ public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable 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() {