diff --git a/src/Prospect.Unreal/Core/Math/FTransform.cs b/src/Prospect.Unreal/Core/Math/FTransform.cs index 8312b37..aa85f8f 100644 --- a/src/Prospect.Unreal/Core/Math/FTransform.cs +++ b/src/Prospect.Unreal/Core/Math/FTransform.cs @@ -1,6 +1,6 @@ namespace Prospect.Unreal.Core.Math; -public ref struct FTransform +public class FTransform { public FVector Location { get; set; } public FRotator Rotation { get; set; } diff --git a/src/Prospect.Unreal/Core/Objects/FObjectInstancingGraph.cs b/src/Prospect.Unreal/Core/Objects/FObjectInstancingGraph.cs new file mode 100644 index 0000000..0633b9e --- /dev/null +++ b/src/Prospect.Unreal/Core/Objects/FObjectInstancingGraph.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Core.Objects; + +public struct FObjectInstancingGraph +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/Objects/GUClassArray.cs b/src/Prospect.Unreal/Core/Objects/GUClassArray.cs index a8c7765..5e0e3e5 100644 --- a/src/Prospect.Unreal/Core/Objects/GUClassArray.cs +++ b/src/Prospect.Unreal/Core/Objects/GUClassArray.cs @@ -13,6 +13,6 @@ public class GUClassArray public static UClass StaticClass(Type type) { // TODO: Implement - return new UClass(); + return new UClass(type); } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/Objects/UClass.cs b/src/Prospect.Unreal/Core/Objects/UClass.cs index a122205..77f7062 100644 --- a/src/Prospect.Unreal/Core/Objects/UClass.cs +++ b/src/Prospect.Unreal/Core/Objects/UClass.cs @@ -4,5 +4,30 @@ namespace Prospect.Unreal.Core.Objects; public class UClass : UStruct { + public UClass(Type type) + { + Type = type; + } + public Type Type { get; } + + public UObject GetDefaultObject() where T : UObject + { + // TODO: Implement + var obj = (UObject) Activator.CreateInstance(); + + obj.SetFlags(EObjectFlags.RF_Public | EObjectFlags.RF_ClassDefaultObject | EObjectFlags.RF_ArchetypeObject); + + return obj; + } + + public UObject GetDefaultObject(bool bCreateIfNeeded = true) + { + throw new NotImplementedException(); + } + + public UObject CreateDefaultObject() + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/Objects/UObject.cs b/src/Prospect.Unreal/Core/Objects/UObject.cs index 0b26624..1198d4a 100644 --- a/src/Prospect.Unreal/Core/Objects/UObject.cs +++ b/src/Prospect.Unreal/Core/Objects/UObject.cs @@ -2,5 +2,9 @@ public class UObject : UObjectBaseUtility { - + public bool CheckDefaultSubobjects(bool bForceCheck = false) + { + // TODO: Implement + return true; + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/Objects/UObjectBaseUtility.cs b/src/Prospect.Unreal/Core/Objects/UObjectBaseUtility.cs index 237dfd1..7db9f7b 100644 --- a/src/Prospect.Unreal/Core/Objects/UObjectBaseUtility.cs +++ b/src/Prospect.Unreal/Core/Objects/UObjectBaseUtility.cs @@ -39,6 +39,12 @@ public class UObjectBaseUtility : UObjectBase // TODO: GUObjectArray return false; } + + public bool IsPendingKill() + { + // TODO: GUObjectArray + return false; + } /* * Outer & Package diff --git a/src/Prospect.Unreal/Core/Objects/UObjectGlobals.cs b/src/Prospect.Unreal/Core/Objects/UObjectGlobals.cs new file mode 100644 index 0000000..6bde7b1 --- /dev/null +++ b/src/Prospect.Unreal/Core/Objects/UObjectGlobals.cs @@ -0,0 +1,34 @@ +using Prospect.Unreal.Core.Names; +using Prospect.Unreal.Exceptions; + +namespace Prospect.Unreal.Core.Objects; + +public class UObjectGlobals +{ + public static T? NewObject(UObject outer, + UClass clazz, + FName? name = null, + EObjectFlags flags = EObjectFlags.RF_NoFlags, + UObject? template = null, + bool bCopyTransientsFromClassDefaults = false, + FObjectInstancingGraph? inInstanceGraph = null, + UPackage? externalPackage = null) + { + if (name == null) + { + name = EName.None; + } + + // NewObject + // StaticConstructObject_Internal + // StaticAllocateObject + + var obj = Activator.CreateInstance(clazz.Type); + if (obj == null) + { + throw new UnrealException("Failed to create object."); + } + + return (T?) obj; + } +} \ 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 efdb805..b606cb9 100644 --- a/src/Prospect.Unreal/Net/Actors/AActor.cs +++ b/src/Prospect.Unreal/Net/Actors/AActor.cs @@ -1,4 +1,5 @@ using Prospect.Unreal.Core; +using Prospect.Unreal.Core.Math; using Prospect.Unreal.Core.Objects; using Prospect.Unreal.Runtime; @@ -7,13 +8,20 @@ namespace Prospect.Unreal.Net.Actors; public class AActor : UObject { private bool bActorInitialized; - + private bool bActorIsBeingDestroyed; + // TODO: UPROPERTY(BlueprintReadWrite, ReplicatedUsing=OnRep_Instigator, meta=(ExposeOnSpawn=true, AllowPrivateAccess=true), Category=Actor) /// /// Pawn responsible for damage and other gameplay events caused by this actor. /// private APawn? _instigator; + // TODO: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Actor) + /// + /// Controls how to handle spawning this actor in a situation where it's colliding with something else. "Default" means AlwaysSpawn here. + /// + public ESpawnActorCollisionHandlingMethod SpawnCollisionHandlingMethod { get; set; } + /// /// Sets the value of Role without causing other side effects to this instance. /// @@ -81,4 +89,22 @@ public class AActor : UObject { return bActorInitialized; } + + public bool IsPendingKillPending() + { + return bActorIsBeingDestroyed || IsPendingKill(); + } + + public void PostSpawnInitialize(FTransform userSpawnTransform, AActor? inOwner, AActor? inInstigator, bool bRemoteOwned, bool bNoFail, bool bDeferConstruction) + { + // General flow here is like so + // - Actor sets up the basics. + // - Actor gets PreInitializeComponents() + // - Actor constructs itself, after which its components should be fully assembled + // - Actor components get OnComponentCreated + // - Actor components get InitializeComponent + // - Actor gets PostInitializeComponents() once everything is set up + // + // This should be the same sequence for deferred or nondeferred spawning. + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UNetDriver.cs b/src/Prospect.Unreal/Net/UNetDriver.cs index 2d8a29d..4be9bfe 100644 --- a/src/Prospect.Unreal/Net/UNetDriver.cs +++ b/src/Prospect.Unreal/Net/UNetDriver.cs @@ -2,6 +2,7 @@ using System.Net; using Prospect.Unreal.Core.Names; using Prospect.Unreal.Exceptions; +using Prospect.Unreal.Net.Actors; using Prospect.Unreal.Net.Channels; using Prospect.Unreal.Net.Channels.Actor; using Prospect.Unreal.Net.Channels.Control; @@ -269,6 +270,29 @@ public abstract class UNetDriver : IAsyncDisposable }; } + public void AddNetworkActor(AActor actor) + { + // if (!IsDormInitialStartupActor(Actor)) + // { + // GetNetworkObjectList().FindOrAdd(Actor, this); + // if (ReplicationDriver) + // { + // ReplicationDriver->AddNetworkActor(Actor); + // } + // } + } + + public void RemoveNetworkActor(AActor actor) + { + // Remove from renamed list if destroyed + // RenamedStartupActors.Remove(Actor->GetFName()); + // + // if (ReplicationDriver) + // { + // ReplicationDriver->RemoveNetworkActor(Actor); + // } + } + public virtual ValueTask DisposeAsync() { return ValueTask.CompletedTask; diff --git a/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs b/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs index 51788c6..eb37d7b 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.LevelActor.cs @@ -1,7 +1,9 @@ using System.Text; using Prospect.Unreal.Core; using Prospect.Unreal.Core.Math; +using Prospect.Unreal.Core.Names; using Prospect.Unreal.Core.Objects; +using Prospect.Unreal.Exceptions; using Prospect.Unreal.Net; using Prospect.Unreal.Net.Actors; @@ -63,7 +65,7 @@ public partial class UWorld return SpawnActor(clazz, transform, spawnParameters); } - public AActor SpawnActor(UClass? clazz, FTransform userTransform, FActorSpawnParameters spawnParameters) + public AActor SpawnActor(UClass? clazz, FTransform? userTransformPtr, FActorSpawnParameters spawnParameters) { if (clazz == null) { @@ -84,10 +86,91 @@ public partial class UWorld if (template == null) { - // template = clazz.GetDefaultObject(); + template = (AActor)clazz.GetDefaultObject(); + } + + if (newActorName == EName.None) + { + // If we are using a template object and haven't specified a name, create a name relative to the template, otherwise let the default object naming behavior in Stat + if (!template.HasAnyFlags(EObjectFlags.RF_ClassDefaultObject)) + { + throw new NotImplementedException(); + } + } + /* else if (StaticFindObjectFast(nullptr, LevelToSpawnIn, NewActorName)) */ + + // See if we can spawn on ded.server/client only etc (check NeedsLoadForClient & NeedsLoadForServer) + // TODO: CanCreateInCurrentContext + + var userTransform = userTransformPtr ?? new FTransform(); // TODO: FTransform::Identity + var collisionHandlingOverride = spawnParameters.SpawnCollisionHandlingOverride; + + // "no fail" take preedence over collision handling settings that include fails + if (spawnParameters.bNoFail) + { + // maybe upgrade to disallow fail + if (collisionHandlingOverride == ESpawnActorCollisionHandlingMethod.AdjustIfPossibleButDontSpawnIfColliding) + { + collisionHandlingOverride = ESpawnActorCollisionHandlingMethod.AdjustIfPossibleButAlwaysSpawn; + } + else if (collisionHandlingOverride == ESpawnActorCollisionHandlingMethod.DontSpawnIfColliding) + { + collisionHandlingOverride = ESpawnActorCollisionHandlingMethod.AlwaysSpawn; + } + } + + // use override if set, else fall back to actor's preference + var collisionHandlingMethod = (collisionHandlingOverride == ESpawnActorCollisionHandlingMethod.Undefined) + ? template.SpawnCollisionHandlingMethod + : collisionHandlingOverride; + + // see if we can avoid spawning altogether by checking native components + // note: we can't handle all cases here, since we don't know the full component hierarchy until after the actor is spawned + if (collisionHandlingMethod == ESpawnActorCollisionHandlingMethod.DontSpawnIfColliding) + { + throw new NotImplementedException(); + } + + var actorFlags = spawnParameters.ObjectFlags; + UPackage? externalPackage = null; + + // actually make the actor object + var actor = UObjectGlobals.NewObject(levelToSpawnIn, clazz, newActorName, actorFlags, template, false, null, externalPackage); + + if (actor == null) + { + throw new UnrealException("Failed to create actor"); + } + + if (actor.GetLevel() != levelToSpawnIn) + { + throw new UnrealException("Actor spawned with the incorrect level"); } - return null; + // tell the actor what method to use, in case it was overridden + actor.SpawnCollisionHandlingMethod = collisionHandlingMethod; + + actor.PostSpawnInitialize(userTransform, spawnParameters.Owner, spawnParameters.Instigator, spawnParameters.bRemoteOwned, spawnParameters.bNoFail, spawnParameters.bDeferConstruction); + + // if we are spawning an external actor, clear the dirty flag after post spawn initialize which might have dirtied the level package through running construction scripts + if (externalPackage != null) + { + throw new NotImplementedException(); + } + + if (actor.IsPendingKill() && !spawnParameters.bNoFail) + { + // TODO: GetPathName + Logger.Debug("SpawnActor failed because the spawned actor %s IsPendingKill"); + return null; + } + + actor.CheckDefaultSubobjects(); + + // Add this newly spawned actor to the network actor list. Do this after PostSpawnInitialize so that actor has "finished" spawning. + AddNetworkActor(actor); + + return actor; } public T SpawnActor(UClass? clazz, FActorSpawnParameters spawnParameters) where T : AActor diff --git a/src/Prospect.Unreal/Runtime/UWorld.cs b/src/Prospect.Unreal/Runtime/UWorld.cs index cb95501..0dc631c 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.cs @@ -34,6 +34,8 @@ public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable _owningGameInstance = null; _authorityGameMode = null; _levels = new List(); + + Url = new FUrl(); } /// @@ -64,7 +66,7 @@ public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable /// /// The URL that was used when loading this World. /// - public FUrl? Url { get; private set; } + 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. @@ -156,7 +158,7 @@ public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable var options = inUrl.OptionsToString(); // Set level info. - if (!string.IsNullOrEmpty(inUrl.GetOption("load", null))) + if (string.IsNullOrEmpty(inUrl.GetOption("load", null))) { Url = inUrl; } @@ -446,6 +448,49 @@ public abstract partial class UWorld : FNetworkNotify, IAsyncDisposable connection.SetClientLoginState(EClientLoginState.Welcomed); } + private void AddNetworkActor(AActor? actor) + { + if (actor == null) + { + Logger.Verbose("Failed to add actor, null"); + return; + } + + if (actor.IsPendingKillPending()) + { + Logger.Verbose("Failed to add actor, IsPendingKillPending"); + return; + } + + var level = actor.GetLevel(); + if (level == null || !ContainsLevel(level)) + { + Logger.Verbose("Failed to add actor, world does not contain level"); + return; + } + + if (NetDriver != null) + { + NetDriver.AddNetworkActor(actor); + } + } + + private void RemoveNetworkActor(AActor? actor) + { + if (actor != null) + { + if (NetDriver != null) + { + NetDriver.RemoveNetworkActor(actor); + } + } + } + + private bool ContainsLevel(ULevel inLevel) + { + return _levels.Contains(inLevel); + } + public bool IsServer() { if (NetDriver != null)