diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index dd2a8577..fcc71ff1 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -29,6 +29,25 @@ export class Room { this.hash = idWithHash.substr(indexOfHash + 1); } } + + public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} { + let roomId = ''; + let hash = ''; + if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link + const absoluteExitSceneUrl = new URL(identifier, baseUrl); + roomId = '_/'+currentInstance+'/'+absoluteExitSceneUrl.hostname + absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId + hash = absoluteExitSceneUrl.hash; + hash = hash.substring(1); //remove the leading diese + } else { //absolute room Id + const parts = identifier.split('#'); + roomId = parts[0]; + roomId = roomId.substring(1); //remove the leading slash + if (parts.length > 1) { + hash = parts[1] + } + } + return {roomId, hash} + } public async getMapUrl(): Promise { return new Promise((resolve, reject) => { diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index b25e2d76..51b9831f 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -52,6 +52,7 @@ export class RoomConnection implements RoomConnection { private static websocketFactory: null|((url: string)=>any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any private closed: boolean = false; private tags: string[] = []; + private intervalId!: NodeJS.Timeout; public static setWebsocketFactory(websocketFactory: (url: string)=>any): void { // eslint-disable-line @typescript-eslint/no-explicit-any RoomConnection.websocketFactory = websocketFactory; @@ -89,8 +90,12 @@ export class RoomConnection implements RoomConnection { this.socket.onopen = (ev) => { //we manually ping every 20s to not be logged out by the server, even when the game is in background. const pingMessage = new PingMessage(); - setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); + this.intervalId = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); }; + + this.socket.onclose = () => { + clearTimeout(this.intervalId); + } this.socket.onmessage = (messageEvent) => { const arrayBuffer: ArrayBuffer = messageEvent.data; diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index b9862c49..0d58a4ea 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -39,20 +39,16 @@ export class GameManager { public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise { const roomID = room.id; const mapUrl = await room.getMapUrl(); - console.log('Loading map '+roomID+' at url '+mapUrl); - const gameIndex = scenePlugin.getIndex(mapUrl); + const gameIndex = scenePlugin.getIndex(roomID); if(gameIndex === -1){ const game : Phaser.Scene = GameScene.createFromUrl(room, mapUrl); - console.log('Adding scene '+mapUrl); - scenePlugin.add(mapUrl, game, false); + scenePlugin.add(roomID, game, false); } } - public async goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin) { - const url = await this.startRoom.getMapUrl(); - console.log('Starting scene '+url); - scenePlugin.start(url); + public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void { + scenePlugin.start(this.startRoom.id); } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 45fb6a06..34ede8dc 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -59,6 +59,7 @@ import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMes import {ResizableScene} from "../Login/ResizableScene"; import {Room} from "../../Connexion/Room"; import {jitsiFactory} from "../../WebRtc/JitsiFactory"; +import {urlManager} from "../../Url/UrlManager"; export interface GameSceneInitInterface { initPosition: PointInterface|null @@ -123,7 +124,6 @@ export class GameScene extends ResizableScene implements CenterListener { private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; - MapKey: string; MapUrlFile: string; RoomId: string; instance: string; @@ -137,7 +137,6 @@ export class GameScene extends ResizableScene implements CenterListener { y: -1000 } - private PositionNextScene: Array> = new Array>(); private presentationModeSprite!: Sprite; private chatModeSprite!: Sprite; private gameMap!: GameMap; @@ -146,17 +145,14 @@ export class GameScene extends ResizableScene implements CenterListener { private outlinedItem: ActionableItem|null = null; private userInputManager!: UserInputManager; - static createFromUrl(room: Room, mapUrlFile: string, gameSceneKey: string|null = null): GameScene { + static createFromUrl(room: Room, mapUrlFile: string): GameScene { // We use the map URL as a key - if (gameSceneKey === null) { - gameSceneKey = mapUrlFile; - } - return new GameScene(room, mapUrlFile, gameSceneKey); + return new GameScene(room, mapUrlFile); } - constructor(private room: Room, MapUrlFile: string, gameSceneKey: string) { + constructor(private room: Room, MapUrlFile: string) { super({ - key: gameSceneKey + key: room.id }); this.GameManager = gameManager; @@ -164,7 +160,6 @@ export class GameScene extends ResizableScene implements CenterListener { this.groups = new Map(); this.instance = room.getInstance(); - this.MapKey = MapUrlFile; this.MapUrlFile = MapUrlFile; this.RoomId = room.id; @@ -183,15 +178,15 @@ export class GameScene extends ResizableScene implements CenterListener { file: file.src }); }); - this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); //TODO strategy to add access token - this.load.tilemapTiledJSON(this.MapKey, this.MapUrlFile); + this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered. // In this case, we check in the cache to see if the map is here and trigger the event manually. - if (this.cache.tilemap.exists(this.MapKey)) { - const data = this.cache.tilemap.get(this.MapKey); + if (this.cache.tilemap.exists(this.MapUrlFile)) { + const data = this.cache.tilemap.get(this.MapUrlFile); this.onMapLoad(data); } @@ -302,14 +297,16 @@ export class GameScene extends ResizableScene implements CenterListener { //hook initialisation init(initData : GameSceneInitInterface) { if (initData.initPosition !== undefined) { - this.initPosition = initData.initPosition; + this.initPosition = initData.initPosition; //todo: still used? } } //hook create scene create(): void { + urlManager.editUrlForCurrentRoom(this.room); + //initalise map - this.Map = this.add.tilemap(this.MapKey); + this.Map = this.add.tilemap(this.MapUrlFile); this.gameMap = new GameMap(this.mapFile); const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { @@ -319,25 +316,21 @@ export class GameScene extends ResizableScene implements CenterListener { //permit to set bound collision this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); - // Let's alter browser history - let path = this.room.id; - if (this.room.hash) { - path += '#' + this.room.hash; - } - window.history.pushState({}, 'WorkAdventure', path); - //add layer on map this.Layers = new Array(); let depth = -2; for (const layer of this.mapFile.layers) { if (layer.type === 'tilelayer') { this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); - } - if (layer.type === 'tilelayer' && this.getExitSceneUrl(layer) !== undefined) { - this.loadNextGameFromExitSceneUrl(layer, this.mapFile.width); - } else if (layer.type === 'tilelayer' && this.getExitUrl(layer) !== undefined) { - console.log('Loading exitUrl ', this.getExitUrl(layer)) - this.loadNextGameFromExitUrl(layer, this.mapFile.width); + + const exitSceneUrl = this.getExitSceneUrl(layer); + if (exitSceneUrl !== undefined) { + this.loadNextGame(exitSceneUrl); + } + const exitUrl = this.getExitUrl(layer); + if (exitUrl !== undefined) { + this.loadNextGame(exitUrl); + } } if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { depth = 10000; @@ -482,7 +475,6 @@ export class GameScene extends ResizableScene implements CenterListener { if (position === undefined) { throw new Error('Position missing from UserMovedMessage'); } - //console.log('Received position ', position.getX(), position.getY(), "from user", message.getUserid()); const messageUserMoved: MessageUserMovedInterface = { userId: message.getUserid(), @@ -515,7 +507,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.simplePeer.unregister(); const gameSceneKey = 'somekey' + Math.round(Math.random() * 10000); - const game: Phaser.Scene = GameScene.createFromUrl(this.room, this.MapUrlFile, gameSceneKey); + const game: Phaser.Scene = GameScene.createFromUrl(this.room, this.MapUrlFile); this.scene.add(gameSceneKey, game, true, { initPosition: { @@ -581,6 +573,12 @@ export class GameScene extends ResizableScene implements CenterListener { } private triggerOnMapLayerPropertyChange(){ + this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { + if (newValue) this.onMapExit(newValue as string); + }); + this.gameMap.onPropertyChange('exitUrl', (newValue, oldValue) => { + if (newValue) this.onMapExit(newValue as string); + }); this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue, allProps) => { if (newValue === undefined) { layoutManager.removeActionButton('openWebsite', this.userInputManager); @@ -635,6 +633,25 @@ export class GameScene extends ResizableScene implements CenterListener { } }); } + + private onMapExit(exitKey: string) { + const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); + //todo: push the hash into the url + if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); + if (roomId !== this.scene.key) { + // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. + this.connection.closeConnection(); + this.simplePeer.unregister(); + this.scene.stop(); + this.scene.remove(this.scene.key); + this.scene.start(roomId, {hash}); + } else { + //if the exit points to the current map, we simply teleport the user back to the startLayer + this.initPositionFromLayerName(this.room.hash || 'start'); + this.CurrentPlayer.x = this.startX; + this.CurrentPlayer.y = this.startY; + } + } private switchLayoutMode(): void { //if discussion is activated, this layout cannot be activated @@ -691,14 +708,13 @@ export class GameScene extends ResizableScene implements CenterListener { return this.getProperty(layer, "exitUrl") as string|undefined; } + /** + * @deprecated the map property exitSceneUrl is deprecated + */ private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { return this.getProperty(layer, "exitSceneUrl") as string|undefined; } - private getExitSceneInstance(layer: ITiledMapLayer): string|undefined { - return this.getProperty(layer, "exitInstance") as string|undefined; - } - private isStartLayer(layer: ITiledMapLayer): boolean { return this.getProperty(layer, "startLayer") == true; } @@ -715,66 +731,11 @@ export class GameScene extends ResizableScene implements CenterListener { return obj.value; } - private loadNextGameFromExitSceneUrl(layer: ITiledMapLayer, mapWidth: number) { - const exitSceneUrl = this.getExitSceneUrl(layer); - if (exitSceneUrl === undefined) { - throw new Error('Layer is not an exit scene layer.'); - } - let instance = this.getExitSceneInstance(layer); - if (instance === undefined) { - instance = this.instance; - } - - const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; - const absoluteExitSceneUrlWithoutProtocol = absoluteExitSceneUrl.toString().substr(absoluteExitSceneUrl.toString().indexOf('://')+3); - const roomId = '_/'+instance+'/'+absoluteExitSceneUrlWithoutProtocol; - - this.loadNextGame(layer, mapWidth, roomId); - } - - private loadNextGameFromExitUrl(layer: ITiledMapLayer, mapWidth: number) { - const exitUrl = this.getExitUrl(layer); - if (exitUrl === undefined) { - throw new Error('Layer is not an exit layer.'); - } - const fullPath = new URL(exitUrl, window.location.toString()).pathname; - - this.loadNextGame(layer, mapWidth, fullPath); - } - //todo: push that into the gameManager - private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){ - + private async loadNextGame(exitSceneIdentifier: string){ + const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const room = new Room(roomId); - gameManager.loadMap(room, this.scene); - const exitSceneKey = roomId; - - const tiles : number[] = layer.data as number[]; - for (let key=0; key < tiles.length; key++) { - const objectKey = tiles[key]; - if(objectKey === 0){ - continue; - } - //key + 1 because the start x = 0; - const y : number = parseInt(((key + 1) / mapWidth).toString()); - const x : number = key - (y * mapWidth); - - let hash = new URL(roomId, this.MapUrlFile).hash; - if (hash) { - hash = hash.substr(1); - } - - //push and save switching case - if (this.PositionNextScene[y] === undefined) { - this.PositionNextScene[y] = new Array<{key: string, hash: string}>(); - } - room.getMapUrl().then((url: string) => { - this.PositionNextScene[y][x] = { - key: url, - hash - } - }) - } + await gameManager.loadMap(room, this.scene); } private startUser(layer: ITiledMapLayer): PositionInterface { @@ -975,33 +936,6 @@ export class GameScene extends ResizableScene implements CenterListener { } player.updatePosition(moveEvent); }); - - const nextSceneKey = this.checkToExit(); - if (!nextSceneKey) return; - if (nextSceneKey.key !== this.scene.key) { - // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. - this.connection.closeConnection(); - this.simplePeer.unregister(); - this.scene.stop(); - this.scene.remove(this.scene.key); - this.scene.start(nextSceneKey.key); - } else { - //if the exit points to the current map, we simply teleport the user back to the startLayer - this.initPositionFromLayerName(this.room.hash || 'start'); - this.CurrentPlayer.x = this.startX; - this.CurrentPlayer.y = this.startY; - } - } - - private checkToExit(): {key: string, hash: string} | null { - const x = Math.floor(this.CurrentPlayer.x / 32); - const y = Math.floor(this.CurrentPlayer.y / 32); - - if (this.PositionNextScene[y] !== undefined && this.PositionNextScene[y][x] !== undefined) { - return this.PositionNextScene[y][x]; - } else { - return null; - } } /** diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts index f7eeacce..051d841f 100644 --- a/front/src/Url/UrlManager.ts +++ b/front/src/Url/UrlManager.ts @@ -1,3 +1,4 @@ +import {Room} from "../Connexion/Room"; export enum GameConnexionTypes { anonymous=1, @@ -44,6 +45,15 @@ class UrlManager { history.pushState({}, 'WorkAdventure', newUrl); return newUrl; } + + //todo: is it duplicated with editUrlForRoom() ? + public editUrlForCurrentRoom(room: Room): void { + let path = room.id; + if (room.hash) { + path += '#'+room.hash; + } + history.pushState({}, 'WorkAdventure', path); + } } diff --git a/front/tests/Phaser/Game/RoomTest.ts b/front/tests/Phaser/Game/RoomTest.ts new file mode 100644 index 00000000..40218d53 --- /dev/null +++ b/front/tests/Phaser/Game/RoomTest.ts @@ -0,0 +1,42 @@ +import "jasmine"; +import {Room} from "../../../src/Connexion/Room"; + +describe("Room getIdFromIdentifier()", () => { + it("should work with an absolute room id and no hash as parameter", () => { + const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', '', ''); + expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); + expect(hash).toEqual(''); + }); + it("should work with an absolute room id and a hash as parameters", () => { + const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json#start', '', ''); + expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); + expect(hash).toEqual("start"); + }); + it("should work with an absolute room id, regardless of baseUrl or instance", () => { + const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', 'https://another.domain/_/global/test.json', 'lol'); + expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); + expect(hash).toEqual(''); + }); + + + it("should work with a relative file link and no hash as parameters", () => { + const {roomId, hash} = Room.getIdFromIdentifier('./test2.json', 'https://maps.workadventu.re/test.json', 'global'); + expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); + expect(hash).toEqual(''); + }); + it("should work with a relative file link with no dot", () => { + const {roomId, hash} = Room.getIdFromIdentifier('test2.json', 'https://maps.workadventu.re/test.json', 'global'); + expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); + expect(hash).toEqual(''); + }); + it("should work with a relative file link two levels deep", () => { + const {roomId, hash} = Room.getIdFromIdentifier('../floor1/Floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global'); + expect(roomId).toEqual('_/global/maps.workadventu.re/floor1/Floor1.json'); + expect(hash).toEqual(''); + }); + it("should work with a relative file link and a hash as parameters", () => { + const {roomId, hash} = Room.getIdFromIdentifier('./test2.json#start', 'https://maps.workadventu.re/test.json', 'global'); + expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json'); + expect(hash).toEqual("start"); + }); +}); \ No newline at end of file