diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index be49f5f70d9..865c73cb0b7 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -552,7 +552,7 @@ await MapRendererContainer dynamicWorldDependencies.WorldInfoTool.Initialize(worldInfoHub); - var characterDataPropagationUtility = new CharacterDataPropagationUtility(staticContainer.ComponentsContainer.ComponentPoolsRegistry.AddComponentPool()); + CharacterDataPropagationUtility characterDataPropagationUtility = staticContainer.CharacterDataPropagationUtility; var currentSceneInfo = new CurrentSceneInfo(); diff --git a/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs index ea93667f171..6165c1ab59e 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs @@ -68,6 +68,7 @@ using ECS.Unity.AssetLoad.Cache; using Global.Dynamic; using Utility; +using DCL.Multiplayer.SDK.Systems.GlobalWorld; using MultiplayerPlugin = DCL.PluginSystem.World.MultiplayerPlugin; namespace Global @@ -134,6 +135,7 @@ public class StaticContainer : IDCLPlugin public IGltfContainerAssetsCache GltfContainerAssetsCache { get; private set; } public AssetPreLoadCache AssetPreLoadCache { get; private set; } + public CharacterDataPropagationUtility CharacterDataPropagationUtility { get; private set; } public void Dispose() { @@ -281,6 +283,9 @@ public UniTask InitializeAsync(StaticSettings settings, CancellationToken ct) var promisesAnalyticsPlugin = new PromisesAnalyticsPlugin(debugContainerBuilder); + container.CharacterDataPropagationUtility = new CharacterDataPropagationUtility( + componentsContainer.ComponentPoolsRegistry.AddComponentPool()); + container.ECSWorldPlugins = new IDCLWorldPlugin[] { new GltfContainerPlugin(sharedDependencies, container.CacheCleaner, container.SceneReadinessReportQueue, launchMode, useRemoteAssetBundles, container.WebRequestsContainer.WebRequestController, container.LoadingStatus, container.GltfContainerAssetsCache, appArgs, componentsContainer.ComponentPoolsRegistry.RootContainerTransform()), @@ -317,7 +322,7 @@ public UniTask InitializeAsync(StaticSettings settings, CancellationToken ct) container.SceneRestrictionBusController, web3IdentityProvider), new PointerInputAudioPlugin(container.assetsProvisioner), new MapPinPlugin(globalWorld, container.MapPinsEventBus), - new MultiplayerPlugin(), + new MultiplayerPlugin(globalWorld, playerEntity, container.CharacterDataPropagationUtility), new RealmInfoPlugin(container.RealmData, container.RoomHubProxy), new InputModifierPlugin(globalWorld, container.PlayerEntity, container.SceneRestrictionBusController), new MainCameraPlugin(componentsContainer.ComponentPoolsRegistry, container.assetsProvisioner, container.CacheCleaner, exposedGlobalDataContainer.ExposedCameraData, container.SceneRestrictionBusController, globalWorld), diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneEcsExecutor.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneEcsExecutor.cs index 2ecaea687d0..868c86cdc04 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneEcsExecutor.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneEcsExecutor.cs @@ -1,5 +1,4 @@ using Arch.Core; -using Utility.Multithreading; namespace SceneRunner.Scene { diff --git a/Explorer/Assets/DCL/Multiplayer/AssemblyInfo.cs b/Explorer/Assets/DCL/Multiplayer/AssemblyInfo.cs new file mode 100644 index 00000000000..418dddea609 --- /dev/null +++ b/Explorer/Assets/DCL/Multiplayer/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DCL.EditMode.Tests")] +[assembly: InternalsVisibleTo("DCL.PlayMode.Tests")] diff --git a/Explorer/Assets/DCL/Multiplayer/AssemblyInfo.cs.meta b/Explorer/Assets/DCL/Multiplayer/AssemblyInfo.cs.meta new file mode 100644 index 00000000000..af2846c90dc --- /dev/null +++ b/Explorer/Assets/DCL/Multiplayer/AssemblyInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8a09713d5cd758149a0698befcbdfcce \ No newline at end of file diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/CharacterDataPropagationUtility.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/CharacterDataPropagationUtility.cs index 6f31db4d4df..fd299f798ce 100644 --- a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/CharacterDataPropagationUtility.cs +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/CharacterDataPropagationUtility.cs @@ -23,16 +23,16 @@ public void PropagateGlobalPlayerToScenePlayer(World globalWorld, Entity globalP { Entity targetEntity = sceneFacade.PersistentEntities.Player; - CopyProfileToSceneEntity(globalWorld.Get(globalPlayerEntity), sceneFacade.EcsExecutor, targetEntity); + CopyProfileToSceneEntity(globalWorld.Get(globalPlayerEntity), sceneFacade.EcsExecutor.World, targetEntity); sceneFacade.EcsExecutor.World.Add(targetEntity, new PlayerSceneCRDTEntity(SpecialEntitiesID.PLAYER_ENTITY)); } - public void CopyProfileToSceneEntity(Profile profile, SceneEcsExecutor sceneEcsExecutor, Entity sceneEntity) + public void CopyProfileToSceneEntity(Profile profile, World sceneWorld, Entity sceneEntity) { - if (!sceneEcsExecutor.World.TryGet(sceneEntity, out SDKProfile? profileSDKSubProduct)) + if (!sceneWorld.TryGet(sceneEntity, out SDKProfile? profileSDKSubProduct)) { profileSDKSubProduct = profileSDKSubProductPool.Get(); - sceneEcsExecutor.World.Add(sceneEntity, profileSDKSubProduct); + sceneWorld.Add(sceneEntity, profileSDKSubProduct); } profileSDKSubProduct!.OverrideWith(profile); diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerCRDTEntitiesHandlerSystem.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerCRDTEntitiesHandlerSystem.cs index 91ae5985c77..e2510a1e24b 100644 --- a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerCRDTEntitiesHandlerSystem.cs +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerCRDTEntitiesHandlerSystem.cs @@ -18,7 +18,6 @@ namespace DCL.Multiplayer.SDK.Systems.GlobalWorld { - // Currently implemented to track reserved entities only on the CURRENT SCENE [UpdateInGroup(typeof(PresentationSystemGroup))] [UpdateAfter(typeof(MultiplayerProfilesSystem))] [LogCategory(ReportCategory.PLAYER_SDK_DATA)] @@ -73,27 +72,29 @@ private void ModifyPlayerScene(in CharacterTransform characterTransform, ref Pla private void ResolvePlayerCRDTScene(in CharacterTransform characterTransform, ref PlayerCRDTEntity globalPlayerCRDTEntity, CRDTEntity reservedEntityId) { bool newSceneIsValid = scenesCache.TryGetByParcel(characterTransform.Transform.ParcelPosition(), out ISceneFacade currentScene) - && currentScene.SceneStateProvider.State == SceneState.Running + && currentScene.SceneStateProvider.State.Value() is SceneState.Running or SceneState.Starting && !currentScene.IsEmpty; if (globalPlayerCRDTEntity.SceneFacade != currentScene) { - // Only try to remove component if we have a valid scene world entity - if (globalPlayerCRDTEntity.SceneWorldEntity != Entity.Null) - { - RemoveComponent(globalPlayerCRDTEntity.SceneWorldEntity, ref globalPlayerCRDTEntity, false); - } + if (globalPlayerCRDTEntity.SceneWorldEntity != Entity.Null + && globalPlayerCRDTEntity.SceneFacade is not null) { RemovePlayerFromScene(globalPlayerCRDTEntity.SceneWorldEntity, reservedEntityId, globalPlayerCRDTEntity.SceneFacade); } if (newSceneIsValid) { SceneEcsExecutor sceneEcsExecutor = currentScene.EcsExecutor; - Entity sceneWorldEntity = sceneEcsExecutor.World.Create(); - sceneEcsExecutor.World.Add(sceneWorldEntity, new PlayerSceneCRDTEntity(reservedEntityId)); + bool isLocalPlayer = reservedEntityId.Id == SpecialEntitiesID.PLAYER_ENTITY; + + // LocalPlayerCRDTEntityHandlerSystem creates PlayerSceneCRDTEntity on scene start-up + Entity sceneWorldEntity = isLocalPlayer + ? currentScene.PersistentEntities.Player + : sceneEcsExecutor.World.Create(new PlayerSceneCRDTEntity(reservedEntityId)); globalPlayerCRDTEntity.AssignToScene(currentScene, sceneWorldEntity); } - else { globalPlayerCRDTEntity.RemoveFromScene(); } + else + globalPlayerCRDTEntity.RemoveFromScene(); } } @@ -116,16 +117,8 @@ private void RemoveComponent(Entity entity, ref PlayerCRDTEntity playerCRDTEntit { if (playerCRDTEntity is { AssignedToScene: true, SceneFacade: not null }) { - // Remove from whichever scene it was added. PlayerCRDTEntity is not removed here, - // as the scene-level Writer systems need it to know which CRDT Entity to affect. - // Only post the cleanup intention if the previous scene's world is still running — - // writing to a Disposing/Disposed/error scene world races against its teardown. - if (playerCRDTEntity.SceneWorldEntity != Entity.Null - && playerCRDTEntity.SceneFacade.SceneStateProvider.State == SceneState.Running) - { - SceneEcsExecutor sceneEcsExecutor = playerCRDTEntity.SceneFacade.EcsExecutor; - sceneEcsExecutor.World.Add(playerCRDTEntity.SceneWorldEntity); - } + if (playerCRDTEntity.SceneWorldEntity != Entity.Null) + RemovePlayerFromScene(playerCRDTEntity.SceneWorldEntity, playerCRDTEntity.CRDTEntity, playerCRDTEntity.SceneFacade); if (noLongerExists) FreeReservedEntity(playerCRDTEntity.CRDTEntity.Id); @@ -135,6 +128,20 @@ private void RemoveComponent(Entity entity, ref PlayerCRDTEntity playerCRDTEntit World.Remove(entity); } + private static void RemovePlayerFromScene(Entity sceneWorldEntity, CRDTEntity crdtEntity, ISceneFacade sceneFacade) + { + // Local Player is never removed from the scene world + if (crdtEntity.Id == SpecialEntitiesID.PLAYER_ENTITY) + return; + + SceneState state = sceneFacade.SceneStateProvider.State.Value(); + + if (state != SceneState.Running && state != SceneState.Starting) + return; + + sceneFacade.EcsExecutor.World.Add(sceneWorldEntity); + } + private int ReserveNextFreeEntity() { // All reserved entities are taken diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerProfileDataPropagationSystem.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerProfileDataPropagationSystem.cs index fdd9c80819c..13e1dfc3781 100644 --- a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerProfileDataPropagationSystem.cs +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerProfileDataPropagationSystem.cs @@ -2,6 +2,7 @@ using Arch.System; using Arch.SystemGroups; using Arch.SystemGroups.DefaultSystemGroups; +using DCL.Character.Components; using DCL.Diagnostics; using DCL.Multiplayer.SDK.Components; using DCL.Profiles; @@ -28,11 +29,11 @@ protected override void Update(float t) } [Query] - [None(typeof(DeleteEntityIntention))] + [None(typeof(DeleteEntityIntention), typeof(PlayerComponent))] private void PropagateProfileToScene(Profile profile, in PlayerCRDTEntity playerCRDTEntity) { if ((playerCRDTEntity.IsDirty || profile.IsDirty) && playerCRDTEntity.AssignedToScene) - characterDataPropagationUtility.CopyProfileToSceneEntity(profile, playerCRDTEntity.SceneFacade!.EcsExecutor, playerCRDTEntity.SceneWorldEntity); + characterDataPropagationUtility.CopyProfileToSceneEntity(profile, playerCRDTEntity.SceneFacade!.EcsExecutor.World, playerCRDTEntity.SceneWorldEntity); } } } diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerTransformPropagationSystem.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerTransformPropagationSystem.cs index 98749d7e9a2..a1ae32c25bb 100644 --- a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerTransformPropagationSystem.cs +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/GlobalWorld/PlayerTransformPropagationSystem.cs @@ -43,7 +43,7 @@ private void PropagateTransformToScene(in CharacterTransform characterTransform, World sceneEcsWorld = playerCRDTEntity.SceneFacade!.EcsExecutor.World; // Position is updated to scene-relative on the writer system - if (!sceneEcsWorld.TryGet(playerCRDTEntity.SceneWorldEntity, out SDKTransform? sdkTransform)) + if (!sceneEcsWorld.TryGet(playerCRDTEntity.SceneWorldEntity, out SDKTransform? sdkTransform)) sceneEcsWorld.Add(playerCRDTEntity.SceneWorldEntity, sdkTransform = sdkTransformPool.Get()); sdkTransform!.Position.Value = characterTransform.Transform.position; diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/LocalPlayerCRDTEntityHandlerSystem.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/LocalPlayerCRDTEntityHandlerSystem.cs new file mode 100644 index 00000000000..716c33202be --- /dev/null +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/LocalPlayerCRDTEntityHandlerSystem.cs @@ -0,0 +1,76 @@ +using Arch.Core; +using Arch.SystemGroups; +using CRDT; +using CrdtEcsBridge.Components; +using DCL.Diagnostics; +using DCL.Multiplayer.SDK.Components; +using DCL.Multiplayer.SDK.Systems.GlobalWorld; +using DCL.PluginSystem.World; +using DCL.Profiles; +using ECS.Abstract; +using ECS.Groups; +using SceneRunner.Scene; + +namespace DCL.Multiplayer.SDK.Systems.SceneWorld +{ + /// + /// Propagates the local player's identity and avatar data to every scene world unconditionally, + /// similar to how handles transforms. + /// Initialize() seeds PlayerSceneCRDTEntity and SDKProfile onto PersistentEntities.Player before the JS + /// runtime starts, so that , , + /// and can flush CRDT messages in their own Initialize(). + /// Update() keeps the SDKProfile in sync when Profile.IsDirty — unlike + /// which only targets the assigned scene + /// and which skips the local player entirely (filtered by PlayerComponent). + /// Runs in SyncedPreRenderingSystemGroup before the writer systems so SDKProfile + /// is up-to-date when they flush CRDT. Profile.IsDirty is guaranteed to be set + /// (set in PresentationSystemGroup, reset later in the same PreRenderingSystemGroup). + /// + [UpdateInGroup(typeof(SyncedPreRenderingSystemGroup))] + [UpdateBefore(typeof(WritePlayerIdentityDataSystem))] + [UpdateBefore(typeof(WriteSDKAvatarBaseSystem))] + [UpdateBefore(typeof(WriteAvatarEquippedDataSystem))] + [LogCategory(ReportCategory.PLAYER_SDK_DATA)] + public partial class LocalPlayerCRDTEntityHandlerSystem : BaseUnityLoopSystem + { + private readonly World globalWorld; + private readonly Entity localPlayerEntity; + private readonly CharacterDataPropagationUtility characterDataPropagationUtility; + private readonly PersistentEntities persistentEntities; + + internal LocalPlayerCRDTEntityHandlerSystem( + World world, + World globalWorld, + Entity localPlayerEntity, + CharacterDataPropagationUtility characterDataPropagationUtility, + PersistentEntities persistentEntities) : base(world) + { + this.globalWorld = globalWorld; + this.localPlayerEntity = localPlayerEntity; + this.characterDataPropagationUtility = characterDataPropagationUtility; + this.persistentEntities = persistentEntities; + } + + public override void Initialize() + { + if (!globalWorld.TryGet(localPlayerEntity, out Profile? profile)) + return; + + Entity playerEntity = persistentEntities.Player; + World.Add(playerEntity, new PlayerSceneCRDTEntity(new CRDTEntity(SpecialEntitiesID.PLAYER_ENTITY))); + + characterDataPropagationUtility.CopyProfileToSceneEntity(profile!, World, playerEntity); + } + + protected override void Update(float t) + { + if (!globalWorld.TryGet(localPlayerEntity, out Profile? profile)) + return; + + if (!profile!.IsDirty) + return; + + characterDataPropagationUtility.CopyProfileToSceneEntity(profile, World, persistentEntities.Player); + } + } +} diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/LocalPlayerCRDTEntityHandlerSystem.cs.meta b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/LocalPlayerCRDTEntityHandlerSystem.cs.meta new file mode 100644 index 00000000000..1d167034453 --- /dev/null +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/LocalPlayerCRDTEntityHandlerSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 647be54c9faa59c41b35f0da2557aa2a \ No newline at end of file diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/WriteSDKAvatarBaseSystem.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/WriteSDKAvatarBaseSystem.cs index 19a7041d05d..ff9313f626c 100644 --- a/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/WriteSDKAvatarBaseSystem.cs +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Systems/SceneWorld/WriteSDKAvatarBaseSystem.cs @@ -27,17 +27,22 @@ public WriteSDKAvatarBaseSystem(World world, IECSToCRDTWriter ecsToCRDTWriter) : this.ecsToCRDTWriter = ecsToCRDTWriter; } + public override void Initialize() + { + UpdateAvatarBaseQuery(World, true); + } + protected override void Update(float t) { HandleComponentRemovalQuery(World); - UpdateAvatarBaseQuery(World); + UpdateAvatarBaseQuery(World, false); } [Query] [None(typeof(DeleteEntityIntention))] - private void UpdateAvatarBase(PlayerSceneCRDTEntity playerCRDTEntity, SDKProfile profile) + private void UpdateAvatarBase([Data] bool force, PlayerSceneCRDTEntity playerCRDTEntity, SDKProfile profile) { - if (!profile.IsDirty) return; + if (!force && !profile.IsDirty) return; ecsToCRDTWriter.PutMessage(static (pbComponent, profile) => { diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Tests/PlayerCRDTEntitiesHandlerSystemShould.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Tests/PlayerCRDTEntitiesHandlerSystemShould.cs index a89f5534fd6..2c0ad08f777 100644 --- a/Explorer/Assets/DCL/Multiplayer/SDK/Tests/PlayerCRDTEntitiesHandlerSystemShould.cs +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Tests/PlayerCRDTEntitiesHandlerSystemShould.cs @@ -98,6 +98,9 @@ public void SetupPlayerCRDTEntityForPlayerInsideScene(bool isMainPlayer) Assert.IsTrue(world.TryGet(entity, out PlayerCRDTEntity playerCRDTEntity)); Assert.IsTrue(scene1World.TryGet(playerCRDTEntity.SceneWorldEntity, out PlayerSceneCRDTEntity scenePlayerCRDTEntity)); Assert.AreEqual(playerCRDTEntity.CRDTEntity, scenePlayerCRDTEntity.CRDTEntity); + + if (isMainPlayer) + Assert.AreEqual(scene1Facade.PersistentEntities.Player, playerCRDTEntity.SceneWorldEntity); } [TestCase(true)] @@ -146,8 +149,19 @@ public void RemovePlayerCRDTEntityForPlayersLeavingScene(bool isMainPlayer) Assert.IsTrue(world.TryGet(entity, out PlayerCRDTEntity newState)); Assert.IsFalse(newState.AssignedToScene); - Assert.IsTrue(playerCRDTEntity.SceneFacade.EcsExecutor.World.Has(playerCRDTEntity.SceneWorldEntity)); - Assert.That(playerCRDTEntity.SceneFacade.EcsExecutor.World.Has(playerCRDTEntity.SceneWorldEntity), Is.True); + + if (isMainPlayer) + { + // Local player: PlayerSceneCRDTEntity persists so scene data remains available + Assert.IsFalse(scene1World.Has(playerCRDTEntity.SceneWorldEntity)); + Assert.IsTrue(scene1World.Has(playerCRDTEntity.SceneWorldEntity)); + } + else + { + // Remote player: separate entity gets DeleteEntityIntention + Assert.IsTrue(playerCRDTEntity.SceneFacade.EcsExecutor.World.Has(playerCRDTEntity.SceneWorldEntity)); + Assert.That(playerCRDTEntity.SceneFacade.EcsExecutor.World.Has(playerCRDTEntity.SceneWorldEntity), Is.True); + } } [TestCase(true)] @@ -180,7 +194,21 @@ public void ChangeSceneOnPlayerMove(bool isMainPlayer) Assert.IsTrue(world.TryGet(entity, out playerCRDTEntity)); Assert.That(playerCRDTEntity.SceneFacade, Is.EqualTo(scene2Facade)); Assert.IsTrue(scene2Facade.EcsExecutor.World.Has(playerCRDTEntity.SceneWorldEntity)); - Assert.That(scene1Facade.EcsExecutor.World.Has(scene1Entity), Is.True); + + if (isMainPlayer) + { + // Local player: persistent entity retains PlayerSceneCRDTEntity (not destroyed) + Assert.IsFalse(scene1Facade.EcsExecutor.World.Has(scene1Entity)); + Assert.IsTrue(scene1Facade.EcsExecutor.World.Has(scene1Entity)); + + // New scene uses its persistent player entity + Assert.AreEqual(scene2Facade.PersistentEntities.Player, playerCRDTEntity.SceneWorldEntity); + } + else + { + // Remote player: old entity gets DeleteEntityIntention + Assert.That(scene1Facade.EcsExecutor.World.Has(scene1Entity), Is.True); + } } [Test] @@ -263,7 +291,7 @@ public void TrackReservedCRDTEntityIdsCorrectly() [TestCase(true)] [TestCase(false)] - public void NotAssignPlayerWhenSceneIsStarting(bool isMainPlayer) + public void AssignPlayerWhenSceneIsStarting(bool isMainPlayer) { scene1Facade.SceneStateProvider.State.Returns(new Atomic(SceneState.Starting)); @@ -279,14 +307,14 @@ public void NotAssignPlayerWhenSceneIsStarting(bool isMainPlayer) system.Update(0); Assert.IsTrue(world.TryGet(entity, out PlayerCRDTEntity playerCRDTEntity)); - Assert.IsFalse(playerCRDTEntity.AssignedToScene); - Assert.That(playerCRDTEntity.SceneFacade, Is.Null); - Assert.That(playerCRDTEntity.SceneWorldEntity, Is.EqualTo(Entity.Null)); + Assert.IsTrue(playerCRDTEntity.AssignedToScene); + Assert.That(playerCRDTEntity.SceneFacade, Is.EqualTo(scene1Facade)); + Assert.IsTrue(scene1World.Has(playerCRDTEntity.SceneWorldEntity)); } [TestCase(true)] [TestCase(false)] - public void AssignPlayerWhenSceneTransitionsFromStartingToRunning(bool isMainPlayer) + public void KeepPlayerAssignedWhenSceneTransitionsFromStartingToRunning(bool isMainPlayer) { scene1Facade.SceneStateProvider.State.Returns(new Atomic(SceneState.Starting)); @@ -299,16 +327,16 @@ public void AssignPlayerWhenSceneTransitionsFromStartingToRunning(bool isMainPla if (isMainPlayer) world.Add(entity, new PlayerComponent()); - // First tick: scene still Starting — gate blocks assignment + // First tick: scene is Starting — player is assigned immediately system.Update(0); Assert.IsTrue(world.TryGet(entity, out PlayerCRDTEntity playerCRDTEntity)); - Assert.IsFalse(playerCRDTEntity.AssignedToScene); + Assert.IsTrue(playerCRDTEntity.AssignedToScene); // Scene finishes initializing scene1Facade.SceneStateProvider.State.Returns(new Atomic(SceneState.Running)); - // Next tick: reconciliation query auto-retries and assigns + // Next tick: assignment persists through state transition system.Update(0); Assert.IsTrue(world.TryGet(entity, out playerCRDTEntity)); @@ -344,9 +372,10 @@ public void SkipSceneSideCleanupWhenPreviousSceneIsDisposing() Assert.IsTrue(world.TryGet(entity, out playerCRDTEntity)); Assert.IsFalse(playerCRDTEntity.AssignedToScene); - // Scene-side cleanup write must be skipped: posting DeleteEntityIntention to a - // disposing world would race with its teardown. + // Scene-side cleanup write must be skipped: RemovePlayerFromScene gates on Running, + // so neither DeleteEntityIntention nor PlayerSceneCRDTEntity removal should happen. Assert.That(scene1World.Has(scene1Entity), Is.False); + Assert.That(scene1World.Has(scene1Entity), Is.True); } [Test] diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Tests/SeedLocalPlayerCRDTSystemShould.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Tests/SeedLocalPlayerCRDTSystemShould.cs new file mode 100644 index 00000000000..a4527e051a3 --- /dev/null +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Tests/SeedLocalPlayerCRDTSystemShould.cs @@ -0,0 +1,84 @@ +using Arch.Core; +using CrdtEcsBridge.Components; +using DCL.Multiplayer.SDK.Components; +using DCL.Multiplayer.SDK.Systems.GlobalWorld; +using DCL.Multiplayer.SDK.Systems.SceneWorld; +using DCL.Optimization.Pools; +using DCL.PluginSystem.World; +using DCL.Profiles; +using ECS.TestSuite; +using NSubstitute; +using NUnit.Framework; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace DCL.Multiplayer.SDK.Tests +{ + public class LocalPlayerCRDTEntityHandlerSystemShould : UnitySystemTestBase + { + private const string FAKE_USER_ID = "Ia4Ia5Cth0ulhu2Ftaghn2"; + + private World globalWorld; + private Entity localPlayerEntity; + private Entity persistentPlayerEntity; + private PersistentEntities persistentEntities; + private CharacterDataPropagationUtility characterDataPropagationUtility; + + [OneTimeSetUp] + public void OneTimeSetUp() => + EcsTestsUtils.SetUpFeaturesRegistry(); + + [OneTimeTearDown] + public void OneTimeTearDown() => + EcsTestsUtils.TearDownFeaturesRegistry(); + + [SetUp] + public void Setup() + { + globalWorld = World.Create(); + + localPlayerEntity = globalWorld.Create( + Profile.NewRandomProfile(FAKE_USER_ID) + ); + + IComponentPool pool = Substitute.For>(); + pool.Get().Returns(_ => new SDKProfile()); + characterDataPropagationUtility = new CharacterDataPropagationUtility(pool); + + persistentPlayerEntity = world.Create(); + persistentEntities = new PersistentEntities(persistentPlayerEntity, Entity.Null, Entity.Null, Entity.Null); + + system = new LocalPlayerCRDTEntityHandlerSystem( + world, globalWorld, localPlayerEntity, + characterDataPropagationUtility, persistentEntities); + } + + protected override void OnTearDown() + { + globalWorld.Dispose(); + } + + [Test] + public void SeedPlayerCRDTEntityAndProfile() + { + system.Initialize(); + + Assert.IsTrue(world.Has(persistentPlayerEntity)); + Assert.IsTrue(world.TryGet(persistentPlayerEntity, out PlayerSceneCRDTEntity crdtEntity)); + Assert.AreEqual(SpecialEntitiesID.PLAYER_ENTITY, crdtEntity.CRDTEntity.Id); + + Assert.IsTrue(world.TryGet(persistentPlayerEntity, out SDKProfile sdkProfile)); + Assert.AreEqual(FAKE_USER_ID, sdkProfile!.UserId); + } + + [Test] + public void NotSeedWhenPlayerHasNoProfile() + { + globalWorld.Remove(localPlayerEntity); + + system.Initialize(); + + Assert.IsFalse(world.Has(persistentPlayerEntity)); + } + } +} diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Tests/SeedLocalPlayerCRDTSystemShould.cs.meta b/Explorer/Assets/DCL/Multiplayer/SDK/Tests/SeedLocalPlayerCRDTSystemShould.cs.meta new file mode 100644 index 00000000000..cda1056cf14 --- /dev/null +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Tests/SeedLocalPlayerCRDTSystemShould.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 098f6326ca42e924bbf989e6d7b7d093 \ No newline at end of file diff --git a/Explorer/Assets/DCL/PluginSystem/World/MultiplayerPlugin.cs b/Explorer/Assets/DCL/PluginSystem/World/MultiplayerPlugin.cs index bbcfc051eb1..b834e6ae5c5 100644 --- a/Explorer/Assets/DCL/PluginSystem/World/MultiplayerPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/World/MultiplayerPlugin.cs @@ -1,4 +1,6 @@ +using Arch.Core; using Arch.SystemGroups; +using DCL.Multiplayer.SDK.Systems.GlobalWorld; using DCL.Multiplayer.SDK.Systems.SceneWorld; using DCL.PluginSystem.World.Dependencies; using ECS.LifeCycle; @@ -12,13 +14,26 @@ namespace DCL.PluginSystem.World { public class MultiplayerPlugin : IDCLWorldPluginWithoutSettings { - public void Dispose() + private readonly Arch.Core.World globalWorld; + private readonly Entity localPlayerEntity; + private readonly CharacterDataPropagationUtility characterDataPropagationUtility; + + public MultiplayerPlugin( + Arch.Core.World globalWorld, + Entity localPlayerEntity, + CharacterDataPropagationUtility characterDataPropagationUtility) { - //ignore + this.globalWorld = globalWorld; + this.localPlayerEntity = localPlayerEntity; + this.characterDataPropagationUtility = characterDataPropagationUtility; } + public void Dispose() { } + public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in ECSWorldInstanceSharedDependencies sharedDependencies, in PersistentEntities persistentEntities, List finalizeWorldSystems, List sceneIsCurrentListeners) { + LocalPlayerCRDTEntityHandlerSystem.InjectToWorld(ref builder, globalWorld, localPlayerEntity, characterDataPropagationUtility, persistentEntities); + WritePlayerIdentityDataSystem.InjectToWorld(ref builder, sharedDependencies.EcsToCRDTWriter); WriteSDKAvatarBaseSystem.InjectToWorld(ref builder, sharedDependencies.EcsToCRDTWriter); WriteAvatarEquippedDataSystem.InjectToWorld(ref builder, sharedDependencies.EcsToCRDTWriter);