Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ await MapRendererContainer

dynamicWorldDependencies.WorldInfoTool.Initialize(worldInfoHub);

var characterDataPropagationUtility = new CharacterDataPropagationUtility(staticContainer.ComponentsContainer.ComponentPoolsRegistry.AddComponentPool<SDKProfile>());
CharacterDataPropagationUtility characterDataPropagationUtility = staticContainer.CharacterDataPropagationUtility;

var currentSceneInfo = new CurrentSceneInfo();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -134,6 +135,7 @@ public class StaticContainer : IDCLPlugin<StaticSettings>

public IGltfContainerAssetsCache GltfContainerAssetsCache { get; private set; }
public AssetPreLoadCache AssetPreLoadCache { get; private set; }
public CharacterDataPropagationUtility CharacterDataPropagationUtility { get; private set; }

public void Dispose()
{
Expand Down Expand Up @@ -281,6 +283,9 @@ public UniTask InitializeAsync(StaticSettings settings, CancellationToken ct)

var promisesAnalyticsPlugin = new PromisesAnalyticsPlugin(debugContainerBuilder);

container.CharacterDataPropagationUtility = new CharacterDataPropagationUtility(
componentsContainer.ComponentPoolsRegistry.AddComponentPool<SDKProfile>());

container.ECSWorldPlugins = new IDCLWorldPlugin[]
{
new GltfContainerPlugin(sharedDependencies, container.CacheCleaner, container.SceneReadinessReportQueue, launchMode, useRemoteAssetBundles, container.WebRequestsContainer.WebRequestController, container.LoadingStatus, container.GltfContainerAssetsCache, appArgs, componentsContainer.ComponentPoolsRegistry.RootContainerTransform()),
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Arch.Core;
using Utility.Multithreading;

namespace SceneRunner.Scene
{
Expand Down
4 changes: 4 additions & 0 deletions Explorer/Assets/DCL/Multiplayer/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("DCL.EditMode.Tests")]
[assembly: InternalsVisibleTo("DCL.PlayMode.Tests")]
2 changes: 2 additions & 0 deletions Explorer/Assets/DCL/Multiplayer/AssemblyInfo.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -73,23 +72,34 @@ 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
Comment thread
mikhail-dcl marked this conversation as resolved.
&& !currentScene.IsEmpty;

if (globalPlayerCRDTEntity.SceneFacade != currentScene)
{
// Only try to remove component if we have a valid scene world entity
if (globalPlayerCRDTEntity.SceneWorldEntity != Entity.Null)
if (globalPlayerCRDTEntity.SceneWorldEntity != Entity.Null
&& globalPlayerCRDTEntity.SceneFacade is not null)
{
RemoveComponent(globalPlayerCRDTEntity.SceneWorldEntity, ref globalPlayerCRDTEntity, false);
RemovePlayerFromScene(globalPlayerCRDTEntity.SceneWorldEntity, reservedEntityId, globalPlayerCRDTEntity.SceneFacade);
}

if (newSceneIsValid)
{
SceneEcsExecutor sceneEcsExecutor = currentScene.EcsExecutor;
Entity sceneWorldEntity;

Entity sceneWorldEntity = sceneEcsExecutor.World.Create();
sceneEcsExecutor.World.Add(sceneWorldEntity, new PlayerSceneCRDTEntity(reservedEntityId));
if (reservedEntityId.Id == SpecialEntitiesID.PLAYER_ENTITY)
{
sceneWorldEntity = currentScene.PersistentEntities.Player;

if (!sceneEcsExecutor.World.Has<PlayerSceneCRDTEntity>(sceneWorldEntity))
sceneEcsExecutor.World.Add(sceneWorldEntity, new PlayerSceneCRDTEntity(reservedEntityId));
}
else
{
sceneWorldEntity = sceneEcsExecutor.World.Create();
sceneEcsExecutor.World.Add(sceneWorldEntity, new PlayerSceneCRDTEntity(reservedEntityId));
}
Comment thread
pravusjif marked this conversation as resolved.
Outdated

globalPlayerCRDTEntity.AssignToScene(currentScene, sceneWorldEntity);
}
Expand All @@ -116,15 +126,9 @@ 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)
if (playerCRDTEntity.SceneWorldEntity != Entity.Null)
{
SceneEcsExecutor sceneEcsExecutor = playerCRDTEntity.SceneFacade.EcsExecutor;
sceneEcsExecutor.World.Add<DeleteEntityIntention>(playerCRDTEntity.SceneWorldEntity);
RemovePlayerFromScene(playerCRDTEntity.SceneWorldEntity, playerCRDTEntity.CRDTEntity, playerCRDTEntity.SceneFacade);
}

if (noLongerExists)
Expand All @@ -135,6 +139,26 @@ private void RemoveComponent(Entity entity, ref PlayerCRDTEntity playerCRDTEntit
World.Remove<PlayerCRDTEntity>(entity);
}

private static void RemovePlayerFromScene(Entity sceneWorldEntity, CRDTEntity crdtEntity, ISceneFacade sceneFacade)
{
bool isLocalPlayer = crdtEntity.Id == SpecialEntitiesID.PLAYER_ENTITY;

SceneState state = sceneFacade.SceneStateProvider.State.Value();

if (state != SceneState.Running && state != SceneState.Starting)
return;

SceneEcsExecutor executor = sceneFacade.EcsExecutor;

if (isLocalPlayer)
{
if (executor.World.Has<PlayerSceneCRDTEntity>(sceneWorldEntity))
executor.World.Remove<PlayerSceneCRDTEntity>(sceneWorldEntity);
}
else
executor.World.Add<DeleteEntityIntention>(sceneWorldEntity);
}

private int ReserveNextFreeEntity()
{
// All reserved entities are taken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,7 +29,7 @@ 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Propagates the local player's identity and avatar data to every scene world unconditionally,
/// similar to how <see cref="DCL.CharacterMotion.Systems.WriteMainPlayerTransformSystem" /> handles transforms.
/// Initialize() seeds PlayerSceneCRDTEntity and SDKProfile onto PersistentEntities.Player before the JS
/// runtime starts, so that <see cref="WritePlayerIdentityDataSystem" />, <see cref="WriteSDKAvatarBaseSystem" />,
/// and <see cref="WriteAvatarEquippedDataSystem" /> can flush CRDT messages in their own Initialize().
/// Update() keeps the SDKProfile in sync when Profile.IsDirty — unlike
/// <see cref="PlayerProfileDataPropagationSystem" /> 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).
/// </summary>
[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<Profile>(localPlayerEntity, out Profile? profile))
return;

Entity playerEntity = persistentEntities.Player;
World.Add(playerEntity, new PlayerSceneCRDTEntity(new CRDTEntity(SpecialEntitiesID.PLAYER_ENTITY)));

characterDataPropagationUtility.CopyProfileToSceneEntity(profile!, new SceneEcsExecutor(World), playerEntity);
Comment thread
pravusjif marked this conversation as resolved.
Outdated
}

protected override void Update(float t)
{
if (!globalWorld.TryGet<Profile>(localPlayerEntity, out Profile? profile))
return;

if (!profile!.IsDirty)
return;

characterDataPropagationUtility.CopyProfileToSceneEntity(profile, new SceneEcsExecutor(World), persistentEntities.Player);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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<PBAvatarBase, SDKProfile>(static (pbComponent, profile) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<PlayerSceneCRDTEntity>(playerCRDTEntity.SceneWorldEntity));
Assert.That(playerCRDTEntity.SceneFacade.EcsExecutor.World.Has<DeleteEntityIntention>(playerCRDTEntity.SceneWorldEntity), Is.True);

if (isMainPlayer)
{
// Local player: PersistentEntities.Player has PlayerSceneCRDTEntity removed, not destroyed
Assert.IsFalse(scene1World.Has<DeleteEntityIntention>(playerCRDTEntity.SceneWorldEntity));
Assert.IsFalse(scene1World.Has<PlayerSceneCRDTEntity>(playerCRDTEntity.SceneWorldEntity));
Comment thread
mikhail-dcl marked this conversation as resolved.
Outdated
}
else
{
// Remote player: separate entity gets DeleteEntityIntention
Assert.IsTrue(playerCRDTEntity.SceneFacade.EcsExecutor.World.Has<PlayerSceneCRDTEntity>(playerCRDTEntity.SceneWorldEntity));
Assert.That(playerCRDTEntity.SceneFacade.EcsExecutor.World.Has<DeleteEntityIntention>(playerCRDTEntity.SceneWorldEntity), Is.True);
}
}

[TestCase(true)]
Expand Down Expand Up @@ -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<PlayerSceneCRDTEntity>(playerCRDTEntity.SceneWorldEntity));
Assert.That(scene1Facade.EcsExecutor.World.Has<DeleteEntityIntention>(scene1Entity), Is.True);

if (isMainPlayer)
{
// Local player: old scene's persistent player loses PlayerSceneCRDTEntity (not destroyed)
Assert.IsFalse(scene1Facade.EcsExecutor.World.Has<DeleteEntityIntention>(scene1Entity));
Assert.IsFalse(scene1Facade.EcsExecutor.World.Has<PlayerSceneCRDTEntity>(scene1Entity));
Comment thread
mikhail-dcl marked this conversation as resolved.
Outdated

// 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<DeleteEntityIntention>(scene1Entity), Is.True);
}
}

[Test]
Expand Down Expand Up @@ -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<DeleteEntityIntention>(scene1Entity), Is.False);
Assert.That(scene1World.Has<PlayerSceneCRDTEntity>(scene1Entity), Is.True);
}

[Test]
Expand Down
Loading
Loading