Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using DCL.Multiplayer.Connections.Messaging.Pipe;
using DCL.Multiplayer.Connections.Rooms.Connective;
using System;
using System.Threading;

namespace CrdtEcsBridge.JsModulesImplementation.Communications
Expand Down Expand Up @@ -31,6 +33,19 @@ public enum ConnectivityAssertiveness

void RemoveSceneMessageHandler(string sceneId, MsgType msgType, SceneMessageHandler onSceneMessage);

/// <summary>
/// Routes the given scene's comms to a dedicated room (its own message pipe) instead of the host's
/// current scene room. Idempotent. The caller owns the room and pipe lifecycle; <see cref="RemoveSceneRoom" />
/// only stops routing and removes this pipe's inbound subscription.
/// </summary>
void RegisterSceneRoom(string sceneId, IConnectiveRoom room, IMessagePipe roomPipe);

/// <summary>
/// Stops routing for a previously registered scene room and unsubscribes from its inbound pipe. Disposing
/// the underlying room/pipe is the caller's responsibility. No-op if the scene was never registered.
/// </summary>
void RemoveSceneRoom(string sceneId);

void SendMessage(ReadOnlySpan<byte> message, string sceneId, ConnectivityAssertiveness assertiveness, CancellationToken ct, string? specialRecipient = null);

readonly ref struct DecodedMessage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using DCL.Multiplayer.Connections.GateKeeper.Rooms;
using DCL.Multiplayer.Connections.GateKeeper.Rooms;
using DCL.Multiplayer.Connections.Messaging;
using DCL.Multiplayer.Connections.Messaging.Hubs;
using DCL.Multiplayer.Connections.Messaging.Pipe;
using DCL.Multiplayer.Connections.Rooms.Connective;
using Decentraland.Kernel.Comms.Rfc4;
using Google.Protobuf;
using DCL.LiveKit.Public;
Expand All @@ -24,13 +25,51 @@ public class SceneCommunicationPipe : ISceneCommunicationPipe
private readonly IGateKeeperSceneRoom sceneRoom;
private readonly IMessagePipe messagePipe;

// Additional per-scene rooms (e.g. authoritative Portable Experience scene rooms) keyed by sceneId. When a
// sceneId has an entry here, its comms are routed to/from that room instead of the host's current scene room.
private readonly Dictionary<string, RoomChannel> extraRoomsBySceneId = new ();

public SceneCommunicationPipe(IMessagePipesHub messagePipesHub, IGateKeeperSceneRoom sceneRoom)
{
this.sceneRoom = sceneRoom;
messagePipe = messagePipesHub.ScenePipe();
messagePipe.Subscribe<Scene>(Packet.MessageOneofCase.Scene, InvokeSubscriber, IMessagePipe.ThreadStrict.ORIGIN_THREAD);
}

/// <summary>
/// Routes the given scene's comms to a dedicated room (its own message pipe) instead of the host's current
/// scene room. Idempotent. The caller owns the room and pipe lifecycle; <see cref="RemoveSceneRoom" /> only
/// stops routing and unsubscribes from the pipe.
/// </summary>
public void RegisterSceneRoom(string sceneId, IConnectiveRoom room, IMessagePipe roomPipe)
{
lock (extraRoomsBySceneId)
{
if (extraRoomsBySceneId.ContainsKey(sceneId)) return;

// Subscribe before publishing the entry so the inbound subscription is active the moment SendMessage
// (called from the scene thread) can find the entry and route outbound to this pipe. The pipe is
// freshly created for and exclusively owned by this scene, so the single subscription MessagePipe
// allows per message type always succeeds here.
roomPipe.Subscribe<Scene>(Packet.MessageOneofCase.Scene, InvokeSubscriber, IMessagePipe.ThreadStrict.ORIGIN_THREAD);
extraRoomsBySceneId[sceneId] = new RoomChannel(room, roomPipe);
}
}

/// <summary>
/// Stops routing for a previously registered scene room and unsubscribes <see cref="InvokeSubscriber" />
/// from its pipe. Disposing the underlying room/pipe is the caller's responsibility. No-op if not registered.
/// </summary>
public void RemoveSceneRoom(string sceneId)
{
lock (extraRoomsBySceneId)
{
if (!extraRoomsBySceneId.Remove(sceneId, out RoomChannel channel)) return;

channel.Pipe.Unsubscribe<Scene>(Packet.MessageOneofCase.Scene, InvokeSubscriber);
}
}

private void InvokeSubscriber(ReceivedMessage<Scene> message)
{
using (message)
Expand All @@ -44,7 +83,7 @@ private void InvokeSubscriber(ReceivedMessage<Scene> message)

// TODO: If the room is connected but the scene is not connected **yet** the message will be skipped and forgotten

if (!sceneRoom.IsSceneConnected(message.Payload.SceneId)) return;
if (!IsSceneConnected(message.Payload.SceneId)) return;

SubscriberKey key = new (message.Payload.SceneId, msgType);

Expand Down Expand Up @@ -108,9 +147,23 @@ public void RemoveSceneMessageHandler(string sceneId, ISceneCommunicationPipe.Ms

public void SendMessage(ReadOnlySpan<byte> message, string sceneId, ISceneCommunicationPipe.ConnectivityAssertiveness assertiveness, CancellationToken ct, string? specialRecipient = null)
{
if (!sceneRoom.IsSceneConnected(sceneId)) return;
IMessagePipe pipe;

lock (extraRoomsBySceneId)
{
if (extraRoomsBySceneId.TryGetValue(sceneId, out RoomChannel channel))
{
if (!IsRoomConnected(channel.Room)) return;
pipe = channel.Pipe;
}
else
{
if (!sceneRoom.IsSceneConnected(sceneId)) return;
pipe = messagePipe;
}
}

MessageWrap<Scene> sceneMessage = messagePipe.NewMessage<Scene>();
MessageWrap<Scene> sceneMessage = pipe.NewMessage<Scene>();

if (!string.IsNullOrEmpty(specialRecipient))
sceneMessage.AddSpecialRecipient(specialRecipient);
Expand All @@ -120,6 +173,19 @@ public void SendMessage(ReadOnlySpan<byte> message, string sceneId, ISceneCommun
sceneMessage.SendAndDisposeAsync(ct, LKDataPacketKind.KindReliable).Forget();
}

private bool IsSceneConnected(string sceneId)
{
lock (extraRoomsBySceneId)
if (extraRoomsBySceneId.TryGetValue(sceneId, out RoomChannel channel))
return IsRoomConnected(channel.Room);

return sceneRoom.IsSceneConnected(sceneId);
}

private static bool IsRoomConnected(IConnectiveRoom room) =>
room.CurrentState() == IConnectiveRoom.State.Running
&& room.Room().Info.ConnectionState == LKConnectionState.ConnConnected;

private readonly struct SubscriberKey : IEquatable<SubscriberKey>
{
public readonly string SceneId;
Expand All @@ -140,5 +206,17 @@ public override bool Equals(object? obj) =>
public override int GetHashCode() =>
HashCode.Combine(SceneId, (int)MsgType);
}

private readonly struct RoomChannel
{
public readonly IConnectiveRoom Room;
public readonly IMessagePipe Pipe;

public RoomChannel(IConnectiveRoom room, IMessagePipe pipe)
{
Room = room;
Pipe = pipe;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@ public void SendMessage(ReadOnlySpan<byte> message, string sceneId, ISceneCommun
{
sendMessageCalls.Add(message.ToArray());
}

public void RegisterSceneRoom(string sceneId, DCL.Multiplayer.Connections.Rooms.Connective.IConnectiveRoom room, DCL.Multiplayer.Connections.Messaging.Pipe.IMessagePipe roomPipe) { }

public void RemoveSceneRoom(string sceneId) { }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ Entity playerEntity
dynamicWorldContainer.ProfileRepository,
dynamicWorldContainer.RoomHub,
dynamicWorldContainer.MvcManager,
dynamicWorldContainer.MessagePipesHub,
dynamicWorldContainer.SceneCommunicationPipe,
dynamicWorldContainer.RemoteMetadata,
webJsSources,
bootstrapContainer.Environment,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Arch.Core;
using CommunicationData.URLHelpers;
using CrdtEcsBridge.JsModulesImplementation.Communications;
using DCL.AssetsProvision;
using DCL.DebugUtilities;
using DCL.LiveKit.Public;
Expand Down Expand Up @@ -68,6 +69,8 @@ public class CommsContainer : IDisposable

public CurrentSceneInfo CurrentSceneInfo { get; }

public SceneCommunicationPipe SceneCommunicationPipe { get; }

private CommsContainer(
IArchipelagoIslandRoom archipelagoIslandRoom,
IGateKeeperSceneRoom gateKeeperSceneRoom,
Expand All @@ -83,7 +86,8 @@ private CommsContainer(
RemoteEntities remoteEntities,
IRemoteMetadata remoteMetadata,
IHealthCheck livekitHealthCheck,
CurrentSceneInfo currentSceneInfo)
CurrentSceneInfo currentSceneInfo,
SceneCommunicationPipe sceneCommunicationPipe)
{
this.archipelagoIslandRoom = archipelagoIslandRoom;
this.gateKeeperSceneRoom = gateKeeperSceneRoom;
Expand All @@ -100,6 +104,7 @@ private CommsContainer(
RemoteMetadata = remoteMetadata;
LivekitHealthCheck = livekitHealthCheck;
CurrentSceneInfo = currentSceneInfo;
SceneCommunicationPipe = sceneCommunicationPipe;
}

public static CommsContainer Create(
Expand Down Expand Up @@ -201,6 +206,8 @@ public static CommsContainer Create(
? livekitHealthCheck.WithFailAnalytics(bootstrapContainer.Analytics.Controller)
: livekitHealthCheck;

var sceneCommunicationPipe = new SceneCommunicationPipe(messagePipesHub, roomHub.SceneRoom());

return new CommsContainer(
archipelagoIslandRoom,
gateKeeperSceneRoom,
Expand All @@ -216,7 +223,8 @@ public static CommsContainer Create(
remoteEntities,
remoteMetadata,
livekitHealthCheck,
new CurrentSceneInfo());
new CurrentSceneInfo(),
sceneCommunicationPipe);
}

public MultiplayerPlugin CreateMultiplayerPlugin(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
using DCL.Optimization.AdaptivePerformance.Systems;
using DCL.PerformanceAndDiagnostics.Analytics;
using DCL.PerformanceAndDiagnostics.Analytics.DecoratorBased;
using CrdtEcsBridge.JsModulesImplementation.Communications;
using DCL.PluginSystem;
using DCL.PluginSystem.Global;
using DCL.PluginSystem.SmartWearables;
Expand Down Expand Up @@ -130,6 +131,8 @@ public class DynamicWorldContainer : DCLWorldContainer<DynamicWorldSettings>

public IRoomHub RoomHub => commsContainer.RoomHub;

public SceneCommunicationPipe SceneCommunicationPipe => commsContainer.SceneCommunicationPipe;

public ISystemClipboard SystemClipboard => uiShellContainer.Clipboard;

private DynamicWorldContainer(
Expand Down Expand Up @@ -442,7 +445,7 @@ await MapRendererContainer
{
new AvatarAttachPlugin(globalWorld, staticContainer.MainPlayerAvatarBaseProxy, staticContainer.ComponentsContainer.ComponentPoolsRegistry, commsContainer.EntityParticipantTable, staticContainer.CharacterContainer.Transform),
new SceneMaskedEmotePlugin(globalWorld, playerEntity, staticContainer.MainPlayerAvatarBaseProxy, staticContainer.EmotesContainer.EmotePlayer, staticContainer.EmoteStorage, multiplayerEmotesMessageBus),
new RealmInfoPlugin(staticContainer.RealmData, commsContainer.RoomHub),
new RealmInfoPlugin(staticContainer.RealmData, commsContainer.RoomHub, staticContainer.ScenesCache),
};

var characterPreviewEventBus = new CharacterPreviewEventBus();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public ECSPortableExperiencesController(
ISceneFacade parentScene = scenesCache.Scenes.FirstOrDefault(s => s.SceneStateProvider.IsCurrent);
string parentSceneName = parentScene != null ? parentScene.Info.Name : "main";
Entity portableExperienceEntity = world.Create();
world.Add(portableExperienceEntity, new PortableExperienceRealmComponent(realmData, parentSceneName, isGlobalPortableExperience), new PortableExperienceComponent(ens));
world.Add(portableExperienceEntity, new PortableExperienceRealmComponent(realmData, parentSceneName, isGlobalPortableExperience), new PortableExperienceComponent(ens, realmData));
world.Add(portableExperienceEntity, new PortableExperienceMetadata
{
Type = isGlobalPortableExperience ? PortableExperienceType.GLOBAL : PortableExperienceType.LOCAL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using CrdtEcsBridge.PoolsProviders;
using DCL.PluginSystem.World;
using DCL.Multiplayer.Connections.DecentralandUrls;
using DCL.Multiplayer.Connections.Messaging.Hubs;
using DCL.Multiplayer.Connections.RoomHubs;
using DCL.Multiplayer.Profiles.Poses;
using DCL.PluginSystem.World.Dependencies;
Expand Down Expand Up @@ -44,7 +43,7 @@ public static SceneSharedContainer Create(in StaticContainer staticContainer,
IProfileRepository profileRepository,
IRoomHub roomHub,
IMVCManager mvcManager,
IMessagePipesHub messagePipesHub,
ISceneCommunicationPipe sceneCommunicationPipe,
IRemoteMetadata remoteMetadata,
IWebJsSources webJsSources,
DecentralandEnvironment dclEnvironment,
Expand Down Expand Up @@ -83,7 +82,7 @@ public static SceneSharedContainer Create(in StaticContainer staticContainer,
realmData,
staticContainer.PortableExperiencesController,
staticContainer.StaticSettings.SkyboxSettings,
new SceneCommunicationPipe(messagePipesHub, roomHub.SceneRoom()),
sceneCommunicationPipe,
remoteMetadata,
dclEnvironment,
systemClipboard,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using DCL.Diagnostics;
using DCL.FeatureFlags;
using DCL.Multiplayer.Connections.DecentralandUrls;
using CrdtEcsBridge.JsModulesImplementation.Communications;
using DCL.Multiplayer.Connections.Messaging.Hubs;
using DCL.Multiplayer.Connections.RoomHubs;
using DCL.Multiplayer.Profiles.Poses;
Expand Down Expand Up @@ -138,7 +139,7 @@ public static class IntegrationTestsSuite
new CancellationTokenSource(),
Substitute.For<IPopupCloserView>()
),
new IMessagePipesHub.Fake(),
Substitute.For<ISceneCommunicationPipe>(),
Substitute.For<IRemoteMetadata>(),
webJsSources,
DecentralandEnvironment.Org,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,17 @@ private void AddNewSceneDefinitionToList(in Entity entity, in PartitionComponent
{
if (sceneDefinitionComponent.IsPortableExperience)
{
// Authoritative-multiplayer PX scenes carry their world's realm so the load flow can join the
// scene-comms room (which spawns the authoritative server). The realm rides on the entity's
// PortableExperienceComponent; null for ordinary PX leaves the load flow untouched.
IRealmData? portableExperienceRealm = sceneDefinitionComponent.Definition.metadata.authoritativeMultiplayer
&& World.TryGet(entity, out PortableExperienceComponent portableExperience)
? portableExperience.RealmData
: null;

//Portable experiences shouldnt be analyzed. Create straight away
World.Add(entity, AssetPromise<ISceneFacade, GetSceneFacadeIntention>.Create(World,
new GetSceneFacadeIntention(sceneDefinitionComponent, issDescriptor), partitionComponent), SceneLoadingState.CreatePortableExperience());
new GetSceneFacadeIntention(sceneDefinitionComponent, issDescriptor, portableExperienceRealm), partitionComponent), SceneLoadingState.CreatePortableExperience());
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ namespace ECS.SceneLifeCycle.SceneDefinition
{
/// <summary>
/// A component used to indicate which scenes are PX and filter them from certain queries.
/// Also contains an ID to help filter which entities belong to certain PX
/// Also contains an ID to help filter which entities belong to certain PX.
/// Carries the PX world's <see cref="RealmData" /> (name, comms secret) so the scene loading flow
/// can establish the authoritative scene-comms room for the experience.
/// </summary>
public struct PortableExperienceComponent
{
public readonly ENS Ens;
public readonly IRealmData RealmData;

public PortableExperienceComponent(ENS ens)
public PortableExperienceComponent(ENS ens, IRealmData realmData)
{
Ens = ens;
RealmData = realmData;
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ public struct GetSceneFacadeIntention : ILoadingIntention, IEquatable<GetSceneFa
/// </summary>
public readonly ISSDescriptor ISSDescriptor;

public GetSceneFacadeIntention(SceneDefinitionComponent definitionComponent, ISSDescriptor issDescriptor)
/// <summary>
/// Set only for authoritative-multiplayer Portable Experiences: the PX world's realm data, used by the
/// scene loading flow to establish the scene-comms room. Null for ordinary scenes and non-authoritative PX.
/// </summary>
public readonly IRealmData? PortableExperienceRealm;

public GetSceneFacadeIntention(SceneDefinitionComponent definitionComponent, ISSDescriptor issDescriptor, IRealmData? portableExperienceRealm = null)
{
DefinitionComponent = definitionComponent;
ISSDescriptor = issDescriptor;
PortableExperienceRealm = portableExperienceRealm;

// URL = EntityId just for identification, it is used by LoadSystemBase, it won't be used as a URL
CommonArguments = new CommonLoadingArguments(definitionComponent.IpfsPath.EntityId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ public async UniTask<ISceneFacade> FlowAsync(World world, ISceneFactory sceneFac

await UniTask.SwitchToMainThread();

// Authoritative-multiplayer Portable Experiences must join their world scene room before the scene is
// considered loaded: the join is what makes the world-content-server spawn the authoritative server.
// The room is owned and torn down by the facade; a failed connect fails the load.
if (intention.PortableExperienceRealm != null && sceneFacade is PortableExperienceSceneFacade portableExperienceScene)
{
try { await portableExperienceScene.StartAuthoritativeRoomAsync(intention.PortableExperienceRealm, ct); }
catch
{
await sceneFacade.DisposeAsync();
throw;
}
}

sceneFacade.Initialize();
ReportHub.LogProductionInfo($"Loading scene {(sceneFacade.SceneData.IsPortableExperience() ? "(PX)" : "")} '{definition.GetLogSceneName()}' (sdk version: '{sceneData.GetSDKVersion()}') ended");
return sceneFacade;
Expand Down
Loading
Loading