diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 33af483f..ffe3563f 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -7,9 +7,14 @@ import { PositionNotifier } from "./PositionNotifier"; import { Movable } from "_Model/Movable"; import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; import { arrayIntersect } from "../Services/ArrayHelper"; -import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb"; +import { + BatchToPusherMessage, + BatchToPusherRoomMessage, + EmoteEventMessage, + JoinRoomMessage, SubToPusherRoomMessage, VariableMessage +} from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; -import { ZoneSocket } from "src/RoomManager"; +import {RoomSocket, ZoneSocket} from "src/RoomManager"; import { Admin } from "../Model/Admin"; export type ConnectCallback = (user: User, group: Group) => void; @@ -35,7 +40,7 @@ export class GameRoom { private readonly disconnectCallback: DisconnectCallback; private itemsState = new Map(); - private variables = new Map(); + public readonly variables = new Map(); private readonly positionNotifier: PositionNotifier; public readonly roomId: string; @@ -45,6 +50,8 @@ export class GameRoom { private versionNumber: number = 1; private nextUserId: number = 1; + private roomListeners: Set = new Set(); + constructor( roomId: string, connectCallback: ConnectCallback, @@ -312,6 +319,22 @@ export class GameRoom { public setVariable(name: string, value: string): void { this.variables.set(name, value); + + // TODO: should we batch those every 100ms? + const variableMessage = new VariableMessage(); + variableMessage.setName(name); + variableMessage.setValue(value); + + const subMessage = new SubToPusherRoomMessage(); + subMessage.setVariablemessage(variableMessage); + + const batchMessage = new BatchToPusherRoomMessage(); + batchMessage.addPayload(subMessage); + + // Dispatch the message on the room listeners + for (const socket of this.roomListeners) { + socket.write(batchMessage); + } } public addZoneListener(call: ZoneSocket, x: number, y: number): Set { @@ -343,4 +366,12 @@ export class GameRoom { public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) { this.positionNotifier.emitEmoteEvent(user, emoteEventMessage); } + + public addRoomListener(socket: RoomSocket) { + this.roomListeners.add(socket); + } + + public removeRoomListener(socket: RoomSocket) { + this.roomListeners.delete(socket); + } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 2514c576..d4dcc6d4 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -4,7 +4,7 @@ import { AdminMessage, AdminPusherToBackMessage, AdminRoomMessage, - BanMessage, + BanMessage, BatchToPusherRoomMessage, EmotePromptMessage, EmptyMessage, ItemEventMessage, @@ -12,7 +12,7 @@ import { PlayGlobalMessage, PusherToBackMessage, QueryJitsiJwtMessage, - RefreshRoomPromptMessage, + RefreshRoomPromptMessage, RoomMessage, ServerToAdminClientMessage, ServerToClientMessage, SilentMessage, @@ -33,6 +33,7 @@ const debug = Debug("roommanager"); export type AdminSocket = ServerDuplexStream; export type ZoneSocket = ServerWritableStream; +export type RoomSocket = ServerWritableStream; const roomManager: IRoomManagerServer = { joinRoom: (call: UserSocket): void => { @@ -156,6 +157,29 @@ const roomManager: IRoomManagerServer = { }); }, + listenRoom(call: RoomSocket): void { + debug("listenRoom called"); + const roomMessage = call.request; + + socketManager.addRoomListener(call, roomMessage.getRoomid()); + + call.on("cancelled", () => { + debug("listenRoom cancelled"); + socketManager.removeRoomListener(call, roomMessage.getRoomid()); + call.end(); + }); + + call.on("close", () => { + debug("listenRoom connection closed"); + socketManager.removeRoomListener(call, roomMessage.getRoomid()); + }).on("error", (e) => { + console.error("An error occurred in listenRoom stream:", e); + socketManager.removeRoomListener(call, roomMessage.getRoomid()); + call.end(); + }); + + }, + adminRoom(call: AdminSocket): void { console.log("adminRoom called"); @@ -230,7 +254,7 @@ const roomManager: IRoomManagerServer = { ): void { socketManager.dispatchRoomRefresh(call.request.getRoomid()); callback(null, new EmptyMessage()); - }, + } }; export { roomManager }; diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 9f655da3..824c8bfb 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -30,7 +30,7 @@ import { BanUserMessage, RefreshRoomMessage, EmotePromptMessage, - VariableMessage, + VariableMessage, BatchToPusherRoomMessage, SubToPusherRoomMessage, } from "../Messages/generated/messages_pb"; import { User, UserSocket } from "../Model/User"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -49,7 +49,7 @@ import Jwt from "jsonwebtoken"; import { JITSI_URL } from "../Enum/EnvironmentVariable"; import { clientEventsEmitter } from "./ClientEventsEmitter"; import { gaugeManager } from "./GaugeManager"; -import { ZoneSocket } from "../RoomManager"; +import {RoomSocket, ZoneSocket} from "../RoomManager"; import { Zone } from "_Model/Zone"; import Debug from "debug"; import { Admin } from "_Model/Admin"; @@ -66,7 +66,9 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo } export class SocketManager { - private rooms: Map = new Map(); + private rooms = new Map(); + // List of rooms in process of loading. + private roomsPromises = new Map>(); constructor() { clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { @@ -102,6 +104,14 @@ export class SocketManager { roomJoinedMessage.addItem(itemStateMessage); } + for (const [name, value] of room.variables.entries()) { + const variableMessage = new VariableMessage(); + variableMessage.setName(name); + variableMessage.setValue(value); + + roomJoinedMessage.addVariable(variableMessage); + } + roomJoinedMessage.setCurrentuserid(user.id); const serverToClientMessage = new ServerToClientMessage(); @@ -186,23 +196,10 @@ export class SocketManager { } handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage) { - const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage); - try { - // TODO: DISPATCH ON NEW ROOM CHANNEL - - const subMessage = new SubMessage(); - subMessage.setItemeventmessage(itemEventMessage); - - // Let's send the event without using the SocketIO room. - // TODO: move this in the GameRoom class. - for (const user of room.getUsers().values()) { - user.emitInBatch(subMessage); - } - room.setVariable(variableMessage.getName(), variableMessage.getValue()); } catch (e) { - console.error('An error occurred on "item_event"'); + console.error('An error occurred on "handleVariableEvent"'); console.error(e); } } @@ -284,10 +281,18 @@ export class SocketManager { } async getOrCreateRoom(roomId: string): Promise { - //check and create new world for a room - let world = this.rooms.get(roomId); - if (world === undefined) { - world = new GameRoom( + //check and create new room + let room = this.rooms.get(roomId); + if (room === undefined) { + let roomPromise = this.roomsPromises.get(roomId); + if (roomPromise) { + return roomPromise; + } + + // Note: for now, the promise is useless (because this is synchronous, but soon, we will need to + // load the map server side. + + room = new GameRoom( roomId, (user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.disConnectedUser(user, group), @@ -303,9 +308,12 @@ export class SocketManager { this.onEmote(emoteEventMessage, listener) ); gaugeManager.incNbRoomGauge(); - this.rooms.set(roomId, world); + this.rooms.set(roomId, room); + + // TODO: change this the to new Promise()... when the method becomes actually asynchronous + roomPromise = Promise.resolve(room); } - return Promise.resolve(world); + return Promise.resolve(room); } private async joinRoom( @@ -676,6 +684,42 @@ export class SocketManager { room.removeZoneListener(call, x, y); } + async addRoomListener(call: RoomSocket, roomId: string) { + const room = await this.getOrCreateRoom(roomId); + if (!room) { + console.error("In addRoomListener, could not find room with id '" + roomId + "'"); + return; + } + + room.addRoomListener(call); + //const things = room.addZoneListener(call, x, y); + + const batchMessage = new BatchToPusherRoomMessage(); + + for (const [name, value] of room.variables.entries()) { + const variableMessage = new VariableMessage(); + variableMessage.setName(name); + variableMessage.setValue(value); + + const subMessage = new SubToPusherRoomMessage(); + subMessage.setVariablemessage(variableMessage); + + batchMessage.addPayload(subMessage); + } + + call.write(batchMessage); + } + + removeRoomListener(call: RoomSocket, roomId: string) { + const room = this.rooms.get(roomId); + if (!room) { + console.error("In removeRoomListener, could not find room with id '" + roomId + "'"); + return; + } + + room.removeRoomListener(call); + } + public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise { const room = await socketManager.getOrCreateRoom(roomId); @@ -831,6 +875,7 @@ export class SocketManager { emoteEventMessage.setActoruserid(user.id); room.emitEmoteEvent(user, emoteEventMessage); } + } export const socketManager = new SocketManager(); diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 9c61bdf8..d8559aa0 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -34,6 +34,8 @@ import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from " import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import { isSetVariableIframeEvent, SetVariableEvent } from "./Events/SetVariableEvent"; +type AnswererCallback = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|PromiseLike; + /** * Listens to messages from iframes and turn those messages into easy to use observables. * Also allows to send messages to those iframes. @@ -111,12 +113,10 @@ class IframeListener { private sendPlayerMove: boolean = false; - // Note: we are forced to type this in "any" because of https://github.com/microsoft/TypeScript/issues/31904 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private answerers: any = {}; - /*private answerers: { - [key in keyof IframeQueryMap]?: (query: IframeQueryMap[key]['query']) => IframeQueryMap[key]['answer']|PromiseLike - } = {};*/ + // Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904 + private answerers: { + [str in keyof IframeQueryMap]?: unknown + } = {}; init() { @@ -156,7 +156,7 @@ class IframeListener { const queryId = payload.id; const query = payload.query; - const answerer = this.answerers[query.type]; + const answerer = this.answerers[query.type] as AnswererCallback | undefined; if (answerer === undefined) { const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.'; console.error(errorMsg); @@ -432,7 +432,7 @@ class IframeListener { * @param key The "type" of the query we are answering * @param callback */ - public registerAnswerer(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|PromiseLike ): void { + public registerAnswerer(key: T, callback: AnswererCallback ): void { this.answerers[key] = callback; } diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 189aea7c..2f4c414b 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -31,6 +31,7 @@ export enum EventMessage { TELEPORT = "teleport", USER_MESSAGE = "user-message", START_JITSI_ROOM = "start-jitsi-room", + SET_VARIABLE = "set-variable", } export interface PointInterface { @@ -105,6 +106,7 @@ export interface RoomJoinedMessageInterface { //users: MessageUserPositionInterface[], //groups: GroupCreatedUpdatedMessageInterface[], items: { [itemId: number]: unknown }; + variables: Map; } export interface PlayGlobalMessageInterface { diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index b2836a03..53eff010 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -165,6 +165,9 @@ export class RoomConnection implements RoomConnection { } else if (subMessage.hasEmoteeventmessage()) { const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage; emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote()); + } else if (subMessage.hasVariablemessage()) { + event = EventMessage.SET_VARIABLE; + payload = subMessage.getVariablemessage(); } else { throw new Error("Unexpected batch message type"); } @@ -174,6 +177,7 @@ export class RoomConnection implements RoomConnection { } } } else if (message.hasRoomjoinedmessage()) { + console.error('COUCOU') const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage; const items: { [itemId: number]: unknown } = {}; @@ -181,6 +185,11 @@ export class RoomConnection implements RoomConnection { items[item.getItemid()] = JSON.parse(item.getStatejson()); } + const variables = new Map(); + for (const variable of roomJoinedMessage.getVariableList()) { + variables.set(variable.getName(), JSON.parse(variable.getValue())); + } + this.userId = roomJoinedMessage.getCurrentuserid(); this.tags = roomJoinedMessage.getTagList(); @@ -188,6 +197,7 @@ export class RoomConnection implements RoomConnection { connection: this, room: { items, + variables, } as RoomJoinedMessageInterface, }); } else if (message.hasWorldfullmessage()) { @@ -634,6 +644,18 @@ export class RoomConnection implements RoomConnection { }); } + public onSetVariable(callback: (name: string, value: unknown) => void): void { + this.onMessage(EventMessage.SET_VARIABLE, (message: VariableMessage) => { + const name = message.getName(); + const serializedValue = message.getValue(); + let value: unknown = undefined; + if (serializedValue) { + value = JSON.parse(serializedValue); + } + callback(name, value); + }); + } + public hasTag(tag: string): boolean { return this.tags.includes(tag); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 1f326837..3ed0254b 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -707,7 +707,7 @@ export class GameScene extends DirtyScene { }); // Set up variables manager - this.sharedVariablesManager = new SharedVariablesManager(this.connection, this.gameMap); + this.sharedVariablesManager = new SharedVariablesManager(this.connection, this.gameMap, onConnect.room.variables); //this.initUsersPosition(roomJoinedMessage.users); this.connectionAnswerPromiseResolve(onConnect.room); diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index 284dec1d..c73ffac4 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -7,6 +7,7 @@ import type {Subscription} from "rxjs"; import type {GameMap} from "./GameMap"; import type {ITile, ITiledMapObject} from "../Map/ITiledMap"; import type {Var} from "svelte/types/compiler/interfaces"; +import {init} from "svelte/internal"; interface Variable { defaultValue: unknown, @@ -18,7 +19,7 @@ export class SharedVariablesManager { private _variables = new Map(); private variableObjects: Map; - constructor(private roomConnection: RoomConnection, private gameMap: GameMap) { + constructor(private roomConnection: RoomConnection, private gameMap: GameMap, serverVariables: Map) { // We initialize the list of variable object at room start. The objects cannot be edited later // (otherwise, this would cause a security issue if the scripting API can edit this list of objects) this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap); @@ -28,6 +29,22 @@ export class SharedVariablesManager { this._variables.set(name, variableObject.defaultValue); } + // Override default values with the variables from the server: + for (const [name, value] of serverVariables) { + this._variables.set(name, value); + } + + roomConnection.onSetVariable((name, value) => { + console.log('Set Variable received from server'); + this._variables.set(name, value); + + // On server change, let's notify the iframes + iframeListener.setVariable({ + key: name, + value: value, + }) + }); + // When a variable is modified from an iFrame iframeListener.registerAnswerer('setVariable', (event) => { const key = event.key; @@ -48,7 +65,8 @@ export class SharedVariablesManager { } this._variables.set(key, event.value); - // TODO: dispatch to the room connection. + + // Dispatch to the room connection. this.roomConnection.emitSetVariableEvent(key, event.value); }); } diff --git a/maps/tests/Variables/shared_variables.html b/maps/tests/Variables/shared_variables.html new file mode 100644 index 00000000..ae282b1c --- /dev/null +++ b/maps/tests/Variables/shared_variables.html @@ -0,0 +1,41 @@ + + + + + + + + + +
+ + diff --git a/maps/tests/index.html b/maps/tests/index.html index dbcf8287..aba4c41a 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -199,7 +199,15 @@ Success Failure Pending - Testing scripting variables + Testing scripting variables locally + + + + + Success Failure Pending + + + Testing shared scripting variables diff --git a/pusher/src/Model/PusherRoom.ts b/pusher/src/Model/PusherRoom.ts index 1eae7a9f..f0dd0e8f 100644 --- a/pusher/src/Model/PusherRoom.ts +++ b/pusher/src/Model/PusherRoom.ts @@ -13,6 +13,7 @@ import { } from "../Messages/generated/messages_pb"; import Debug from "debug"; import {ClientReadableStream} from "grpc"; +import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; const debug = Debug("room"); @@ -34,8 +35,9 @@ export class PusherRoom { private backConnection!: ClientReadableStream; private isClosing: boolean = false; private listeners: Set = new Set(); + public readonly variables = new Map(); - constructor(public readonly roomId: string, private socketListener: ZoneEventListener, private onBackFailure: (e: Error | null, room: PusherRoom) => void) { + constructor(public readonly roomId: string, private socketListener: ZoneEventListener) { this.public = isRoomAnonymous(roomId); this.tags = []; this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY; @@ -96,7 +98,11 @@ export class PusherRoom { for (const message of batch.getPayloadList()) { if (message.hasVariablemessage()) { const variableMessage = message.getVariablemessage() as VariableMessage; - // We need to dispatch this variable to all the listeners + + // We need to store all variables to dispatch variables later to the listeners + this.variables.set(variableMessage.getName(), variableMessage.getValue()); + + // Let's dispatch this variable to all the listeners for (const listener of this.listeners) { const subMessage = new SubMessage(); subMessage.setVariablemessage(variableMessage); @@ -112,14 +118,22 @@ export class PusherRoom { if (!this.isClosing) { debug("Error on back connection"); this.close(); - this.onBackFailure(e, this); + // Let's close all connections linked to that room + for (const listener of this.listeners) { + listener.disconnecting = true; + listener.end(1011, "Connection error between pusher and back server") + } } }); this.backConnection.on("close", () => { if (!this.isClosing) { debug("Close on back connection"); this.close(); - this.onBackFailure(null, this); + // Let's close all connections linked to that room + for (const listener of this.listeners) { + listener.disconnecting = true; + listener.end(1011, "Connection closed between pusher and back server") + } } }); } @@ -128,10 +142,5 @@ export class PusherRoom { debug("Closing connection to room %s on back server", this.roomId); this.isClosing = true; this.backConnection.cancel(); - - // Let's close all connections linked to that room - for (const listener of this.listeners) { - listener.close(); - } } }