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 @@ -23,16 +23,16 @@ public void PropagateGlobalPlayerToScenePlayer(World globalWorld, Entity globalP
{
Entity targetEntity = sceneFacade.PersistentEntities.Player;

CopyProfileToSceneEntity(globalWorld.Get<Profile>(globalPlayerEntity), sceneFacade.EcsExecutor, targetEntity);
CopyProfileToSceneEntity(globalWorld.Get<Profile>(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);
Expand Down
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,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
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)
{
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();
}
}

Expand All @@ -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<DeleteEntityIntention>(playerCRDTEntity.SceneWorldEntity);
}
if (playerCRDTEntity.SceneWorldEntity != Entity.Null)
RemovePlayerFromScene(playerCRDTEntity.SceneWorldEntity, playerCRDTEntity.CRDTEntity, playerCRDTEntity.SceneFacade);

if (noLongerExists)
FreeReservedEntity(playerCRDTEntity.CRDTEntity.Id);
Expand All @@ -135,6 +128,20 @@ private void RemoveComponent(Entity entity, ref PlayerCRDTEntity playerCRDTEntit
World.Remove<PlayerCRDTEntity>(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<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,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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<SDKTransform>(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;
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(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);
}
}
}

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
Loading
Loading