From af260ad22917b30a819f1a0d2ef788b546d82364 Mon Sep 17 00:00:00 2001 From: Piotr Dobrowolski Date: Sun, 3 Jan 2021 12:23:13 +0100 Subject: [PATCH 01/87] front: add AnimatedTiles plugin --- front/package.json | 1 + front/src/Phaser/Game/GameScene.ts | 5 +++++ front/tsconfig.json | 1 + front/yarn.lock | 4 ++++ 4 files changed, 11 insertions(+) diff --git a/front/package.json b/front/package.json index 09f27ec5..d429bd42 100644 --- a/front/package.json +++ b/front/package.json @@ -27,6 +27,7 @@ "generic-type-guard": "^3.2.0", "google-protobuf": "^3.13.0", "phaser": "^3.22.0", + "phaser-animated-tiles": "Informatic/phaser-animated-tiles#2d5c66a9bc426dd4cb2d856c1d599494a74f8067", "queue-typescript": "^1.0.1", "quill": "^1.3.7", "simple-peer": "^9.6.2", diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 181592e2..ab9232d4 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -68,6 +68,8 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha import {TextureError} from "../../Exception/TextureError"; import {TextField} from "../Components/TextField"; +import AnimatedTiles from "phaser-animated-tiles"; + export interface GameSceneInitInterface { initPosition: PointInterface|null, reconnecting: boolean @@ -114,6 +116,7 @@ export class GameScene extends ResizableScene implements CenterListener { Layers!: Array; Objects!: Array; mapFile!: ITiledMap; + animatedTiles!: AnimatedTiles; groups: Map; startX!: number; startY!: number; @@ -189,6 +192,7 @@ export class GameScene extends ResizableScene implements CenterListener { file: file.src }); }); + this.load.scenePlugin('AnimatedTiles', AnimatedTiles, 'animatedTiles', 'animatedTiles'); this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); @@ -400,6 +404,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.initCamera(); + this.animatedTiles.init(this.Map); this.initCirclesCanvas(); // Let's pause the scene if the connection is not established yet diff --git a/front/tsconfig.json b/front/tsconfig.json index 3fce57ea..fbfc8c07 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -8,6 +8,7 @@ "downlevelIteration": true, "jsx": "react", "allowJs": true, + "esModuleInterop": true, "strict": true, /* Enable all strict type-checking options. */ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ diff --git a/front/yarn.lock b/front/yarn.lock index a31409a6..ea50cc4d 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -3663,6 +3663,10 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +phaser-animated-tiles@Informatic/phaser-animated-tiles#2d5c66a9bc426dd4cb2d856c1d599494a74f8067: + version "2.0.2" + resolved "https://codeload.github.com/Informatic/phaser-animated-tiles/tar.gz/2d5c66a9bc426dd4cb2d856c1d599494a74f8067" + phaser@^3.22.0: version "3.51.0" resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.51.0.tgz#b0c7ee2b21e795830d74f476dd30816a42b023bd" From 3836d5037c95d2ec78a3a8855ed2c7c0df5ed20b Mon Sep 17 00:00:00 2001 From: jonny Date: Wed, 21 Apr 2021 15:51:01 +0200 Subject: [PATCH 02/87] game state can be read out by the client APIs # Conflicts: # front/src/Api/IframeListener.ts # front/src/Phaser/Game/GameScene.ts # front/src/iframe_api.ts --- front/src/Api/Events/ApiGameStateEvent.ts | 11 +++++++++++ front/src/Api/IframeListener.ts | 16 ++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 7 +++++++ front/src/iframe_api.ts | 22 ++++++++++++++++++++++ front/src/utility.ts | 18 ++++++++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 front/src/Api/Events/ApiGameStateEvent.ts create mode 100644 front/src/utility.ts diff --git a/front/src/Api/Events/ApiGameStateEvent.ts b/front/src/Api/Events/ApiGameStateEvent.ts new file mode 100644 index 00000000..2d5ec686 --- /dev/null +++ b/front/src/Api/Events/ApiGameStateEvent.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; + +export const isGameStateEvent = + new tg.IsInterface().withProperties({ + roomId: tg.isString, + data:tg.isObject + }).get(); +/** + * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. + */ +export type GameStateEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index c875ebbb..ef7dc6a3 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,6 +12,8 @@ import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import {scriptUtils} from "./ScriptUtils"; import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { GameStateEvent } from './Events/ApiGameStateEvent'; +import { deepFreezeClone as deepFreezeClone } from '../utility'; /** @@ -52,6 +54,10 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); + + private readonly _gameStateStream: Subject = new Subject(); + public readonly gameStateStream = this._gameStateStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -103,6 +109,8 @@ class IframeListener { } else if (payload.type === 'removeBubble'){ this._removeBubbleStream.next(); + }else if(payload.type=="getState"){ + this._gameStateStream.next(); } } @@ -111,6 +119,14 @@ class IframeListener { } + + sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { + this.postMessage({ + 'type': 'gameState', + 'data': deepFreezeClone(gameStateEvent) + }); + } + /** * Allows the passed iFrame to send/receive messages via the API. */ diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 990f702c..ae9f23b8 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -841,6 +841,13 @@ ${escapedMessage} this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ this.userInputManager.restoreControls(); })); + this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(()=>{ + iframeListener.sendFrozenGameStateEvent({ + roomId:this.RoomId, + data: this.mapFile + }) + })); + let scriptedBubbleSprite : Sprite; this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 18d8d172..b1a8de48 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,6 +9,7 @@ import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent"; import {OpenTabEvent} from "./Api/Events/OpenTabEvent"; import {GoToPageEvent} from "./Api/Events/GoToPageEvent"; import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent"; +import { GameStateEvent, isGameStateEvent } from './Api/Events/ApiGameStateEvent'; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -24,6 +25,7 @@ interface WorkAdventureApi { restorePlayerControl() : void; displayBubble() : void; removeBubble() : void; + getGameState():Promise } declare global { @@ -74,7 +76,23 @@ class Popup { } } + +const stateResolvers:Array<(event:GameStateEvent)=>void> =[] + window.WA = { + + + + getGameState(){ + return new Promise((resolver,thrower)=>{ + stateResolvers.push(resolver); + window.parent.postMessage({ + type:"getState" + },"*") + }) + }, + + /** * Send a message in the chat. * Only the local user will receive this message. @@ -224,6 +242,10 @@ window.addEventListener('message', message => { if (callback) { callback(popup); } + }else if(payload.type=="gameState" && isGameStateEvent(payloadData)){ + stateResolvers.forEach(resolver=>{ + resolver(payloadData); + }) } } diff --git a/front/src/utility.ts b/front/src/utility.ts new file mode 100644 index 00000000..a95da6f8 --- /dev/null +++ b/front/src/utility.ts @@ -0,0 +1,18 @@ +export function deepFreezeClone (obj:T):Readonly { + return deepFreeze(JSON.parse(JSON.stringify(obj))); +} + +function deepFreeze (obj:T):T{ + Object.freeze(obj); + if (obj === undefined) { + return obj; + } + const propertyNames = Object.getOwnPropertyNames(obj) as Array; + propertyNames.forEach(function (prop) { + if (obj[prop] !== null&& (typeof obj[prop] === "object" || typeof obj[prop] === "function") && !Object.isFrozen(obj[prop])) { + deepFreezeClone(obj[prop]); + } + }); + + return obj; +} \ No newline at end of file From 79e530f0e60ac4e6156ad6afadbb3a8259fb6860 Mon Sep 17 00:00:00 2001 From: jonny Date: Tue, 27 Apr 2021 00:04:08 +0200 Subject: [PATCH 03/87] launch jsons + type fixes --- back/.vscode/launch.json | 27 +++++++++++++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 2 +- pusher/.vscode/launch.json | 27 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 back/.vscode/launch.json create mode 100644 pusher/.vscode/launch.json diff --git a/back/.vscode/launch.json b/back/.vscode/launch.json new file mode 100644 index 00000000..77cdeee0 --- /dev/null +++ b/back/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Example", + "type": "node", + "request": "launch", + "runtimeExecutable": "node", + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register/transpile-only" + ], + "args": [ + "server.ts", + "--example", + "hello" + ], + "cwd": "${workspaceRoot}", + "internalConsoleOptions": "openOnSessionStart", + "skipFiles": [ + "/**", + "node_modules/**" + ] + } + ] +} \ No newline at end of file diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 2995fbc0..7c48239b 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -151,7 +151,7 @@ export class GameScene extends ResizableScene implements CenterListener { private GlobalMessageManager!: GlobalMessageManager; public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; private connectionAnswerPromise: Promise; - private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike) => void; + private connectionAnswerPromiseResolve!: (value: RoomJoinedMessageInterface | PromiseLike) => void; // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; diff --git a/pusher/.vscode/launch.json b/pusher/.vscode/launch.json new file mode 100644 index 00000000..2a3c02c2 --- /dev/null +++ b/pusher/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Pusher", + "type": "node", + "request": "launch", + "runtimeExecutable": "node", + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register/transpile-only" + ], + "args": [ + "server.ts", + "--example", + "hello" + ], + "cwd": "${workspaceRoot}", + "internalConsoleOptions": "openOnSessionStart", + "skipFiles": [ + "/**", + "node_modules/**" + ] + } + ] +} \ No newline at end of file From fafaabb6e7226e033c6a132d6f8ab270fcd11e1b Mon Sep 17 00:00:00 2001 From: jonny Date: Tue, 27 Apr 2021 11:59:22 +0200 Subject: [PATCH 04/87] script api can add menu commands # Conflicts: # front/src/Api/IframeListener.ts # front/src/iframe_api.ts --- front/src/Api/Events/MenuItemClickedEvent.ts | 10 ++++++ front/src/Api/Events/MenuItemRegisterEvent.ts | 10 ++++++ front/src/Api/IframeListener.ts | 15 +++++++++ front/src/Phaser/Menu/MenuScene.ts | 33 +++++++++++++++++-- front/src/iframe_api.ts | 21 ++++++++++-- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 front/src/Api/Events/MenuItemClickedEvent.ts create mode 100644 front/src/Api/Events/MenuItemRegisterEvent.ts diff --git a/front/src/Api/Events/MenuItemClickedEvent.ts b/front/src/Api/Events/MenuItemClickedEvent.ts new file mode 100644 index 00000000..dd80c0f2 --- /dev/null +++ b/front/src/Api/Events/MenuItemClickedEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isMenuItemClickedEvent = + new tg.IsInterface().withProperties({ + menuItem: tg.isString + }).get(); +/** + * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. + */ +export type MenuItemClickedEvent = tg.GuardedType; diff --git a/front/src/Api/Events/MenuItemRegisterEvent.ts b/front/src/Api/Events/MenuItemRegisterEvent.ts new file mode 100644 index 00000000..98d4c7d3 --- /dev/null +++ b/front/src/Api/Events/MenuItemRegisterEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isMenuItemRegisterEvent = + new tg.IsInterface().withProperties({ + menutItem: tg.isString + }).get(); +/** + * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. + */ +export type MenuItemRegisterEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index c875ebbb..dbb45db3 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,6 +12,8 @@ import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import {scriptUtils} from "./ScriptUtils"; import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; +import { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; /** @@ -52,6 +54,8 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); + private readonly _registerMenuCommandStream: Subject = new Subject(); + public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -103,6 +107,8 @@ class IframeListener { } else if (payload.type === 'removeBubble'){ this._removeBubbleStream.next(); + } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { + this._registerMenuCommandStream.next(payload.data.menutItem) } } @@ -187,6 +193,15 @@ class IframeListener { this.scripts.delete(scriptUrl); } + sendMenuClickedEvent(menuItem: string) { + this.postMessage({ + 'type': 'menuItemClicked', + 'data': { + menuItem: menuItem, + } as MenuItemClickedEvent + }); + } + sendUserInputChat(message: string) { this.postMessage({ 'type': 'userInputChat', diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 05cea305..9e11a873 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -9,6 +9,9 @@ import {connectionManager} from "../../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../../Url/UrlManager"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; +import { HtmlUtils } from '../../WebRtc/HtmlUtils'; +import { iframeListener } from '../../Api/IframeListener'; +import { Subscription } from 'rxjs'; export const MenuSceneName = 'MenuScene'; const gameMenuKey = 'gameMenu'; @@ -36,11 +39,20 @@ export class MenuScene extends Phaser.Scene { private warningContainer: WarningContainer | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null; + private apiMenus = [] + + + private subscriptions = new Subscription() constructor() { super({key: MenuSceneName}); this.gameQualityValue = localUserStore.getGameQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue(); + + this.subscriptions.add(iframeListener.registerMenuCommandStream.subscribe(menuCommand => { + this.addMenuOption(menuCommand); + + })) } preload () { @@ -266,13 +278,28 @@ export class MenuScene extends Phaser.Scene { }); } - private onMenuClick(event:MouseEvent) { - if((event?.target as HTMLInputElement).classList.contains('not-button')){ + public addMenuOption(menuText: string) { + const wrappingSection = document.createElement("section") + wrappingSection.innerHTML = `` + const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); + if (menuItemContainer) { + menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) + } + } + + private onMenuClick(event: MouseEvent) { + const htmlMenuItem = (event?.target as HTMLInputElement); + if (htmlMenuItem.classList.contains('not-button')) { return; } event.preventDefault(); - switch ((event?.target as HTMLInputElement).id) { + if (htmlMenuItem.classList.contains("fromApi")) { + iframeListener.sendMenuClickedEvent(htmlMenuItem.id) + return + } + + switch (htmlMenuItem.id) { case 'changeNameButton': this.closeSideMenu(); gameManager.leaveGame(this, LoginSceneName, new LoginScene()); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 18d8d172..1b68b0c1 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,6 +9,8 @@ import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent"; import {OpenTabEvent} from "./Api/Events/OpenTabEvent"; import {GoToPageEvent} from "./Api/Events/GoToPageEvent"; import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent"; +import { isMenuItemClickedEvent } from './Api/Events/MenuItemClickedEvent'; +import { MenuItemRegisterEvent } from './Api/Events/MenuItemRegisterEvent'; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -24,6 +26,7 @@ interface WorkAdventureApi { restorePlayerControl() : void; displayBubble() : void; removeBubble() : void; + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void } declare global { @@ -40,7 +43,7 @@ const enterStreams: Map> = new Map> = new Map>(); const popups: Map = new Map(); const popupCallbacks: Map> = new Map>(); - +const menuCallbacks: Map void> = new Map() let popupId = 0; interface ButtonDescriptor { /** @@ -172,6 +175,16 @@ window.WA = { popups.set(popupId, popup) return popup; }, + + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { + menuCallbacks.set(commandDescriptor, callback); + window.parent.postMessage({ + 'type': 'registerMenuCommand', + 'data': { + menutItem: commandDescriptor + } as MenuItemRegisterEvent + }, '*'); + }, /** * Listen to messages sent by the local user, in the chat. */ @@ -224,8 +237,12 @@ window.addEventListener('message', message => { if (callback) { callback(popup); } + } else if (payload.type == "menuItemClicked" && isMenuItemClickedEvent(payload.data)) { + const callback = menuCallbacks.get(payload.data.menuItem); + if (callback) { + callback(payload.data.menuItem) + } } - } // ... From 4069e878721deffa2d0e2b3833f62f05b834aca2 Mon Sep 17 00:00:00 2001 From: jonny Date: Tue, 27 Apr 2021 12:40:29 +0200 Subject: [PATCH 05/87] replace menu items if already present --- front/src/Phaser/Menu/MenuScene.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 9e11a873..348554b3 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -280,9 +280,11 @@ export class MenuScene extends Phaser.Scene { public addMenuOption(menuText: string) { const wrappingSection = document.createElement("section") - wrappingSection.innerHTML = `` + const excapedHtml = HtmlUtils.escapeHtml(menuText); + wrappingSection.innerHTML = `` const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); if (menuItemContainer) { + menuItemContainer.querySelector(`#${excapedHtml}.fromApi`)?.remove() menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) } } From 6295c8275ec6b3b3f71306ac5bb3af2ee4b2ea67 Mon Sep 17 00:00:00 2001 From: jonny Date: Tue, 27 Apr 2021 16:40:56 +0200 Subject: [PATCH 06/87] reset menu items on map change --- front/src/Phaser/Game/GameScene.ts | 3 +++ front/src/Phaser/Menu/MenuScene.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 464c3ca4..1a7a2d9f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -90,6 +90,7 @@ import {LayersIterator} from "../Map/LayersIterator"; import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; +import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -880,6 +881,8 @@ ${escapedMessage} const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); urlManager.pushStartLayerNameToUrl(hash); + const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene + menuScene.reset() if (roomId !== this.scene.key) { if (this.scene.get(roomId) === null) { console.error("next room not loaded", exitKey); diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 348554b3..702fb67b 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -38,10 +38,6 @@ export class MenuScene extends Phaser.Scene { private menuButton!: Phaser.GameObjects.DOMElement; private warningContainer: WarningContainer | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null; - - private apiMenus = [] - - private subscriptions = new Subscription() constructor() { super({key: MenuSceneName}); @@ -64,6 +60,13 @@ export class MenuScene extends Phaser.Scene { this.load.html(warningContainerKey, warningContainerHtml); } + reset() { + const addedMenuItems=[...this.menuElement.node.querySelectorAll(".fromApi")]; + for(let index=addedMenuItems.length-1;index>=0;index--){ + addedMenuItems[index].remove() + } + } + create() { this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey); this.menuElement.setOrigin(0); From cd77af318d779a10deb136b980d5dc1304340f30 Mon Sep 17 00:00:00 2001 From: jonny Date: Sat, 1 May 2021 19:44:14 +0200 Subject: [PATCH 07/87] added more properties # Conflicts: # front/src/Phaser/Game/GameScene.ts --- front/src/Api/Events/ApiGameStateEvent.ts | 21 ++++++++++++++- front/src/Phaser/Game/GameScene.ts | 32 ++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/front/src/Api/Events/ApiGameStateEvent.ts b/front/src/Api/Events/ApiGameStateEvent.ts index 2d5ec686..4f4e98ff 100644 --- a/front/src/Api/Events/ApiGameStateEvent.ts +++ b/front/src/Api/Events/ApiGameStateEvent.ts @@ -1,9 +1,28 @@ import * as tg from "generic-type-guard"; +export const isPositionState = new tg.IsInterface().withProperties({ + x: tg.isNumber, + y: tg.isNumber +}).get() +export const isPlayerState = new tg.IsInterface() + .withStringIndexSignature( + new tg.IsInterface().withProperties({ + position: isPositionState, + pusherId: tg.isUnion(tg.isNumber, tg.isUndefined) + }).get() + ).get() + +export type PlayerStateObject = tg.GuardedType; + export const isGameStateEvent = new tg.IsInterface().withProperties({ roomId: tg.isString, - data:tg.isObject + data: tg.isObject, + mapUrl: tg.isString, + nickName: tg.isString, + uuid: tg.isUnion(tg.isString, tg.isUndefined), + players: isPlayerState, + startLayerName: tg.isUnion(tg.isString, tg.isNull) }).get(); /** * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ae9f23b8..3841ab07 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -80,6 +80,7 @@ import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoading import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; +import { PlayerStateObject } from '../../Api/Events/ApiGameStateEvent'; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -841,10 +842,35 @@ ${escapedMessage} this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ this.userInputManager.restoreControls(); })); - this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { + const playerObject: PlayerStateObject = { + [this.playerName]: { + position: { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y + }, + pusherId: this.connection?.getUserId() + } + } + for (const mapPlayer of this.MapPlayers.children.entries) { + const remotePlayer: RemotePlayer = mapPlayer as RemotePlayer; + playerObject[remotePlayer.PlayerValue] = { + position: { + x: remotePlayer.x, + y: remotePlayer.y + }, + pusherId: remotePlayer.userId + + } + } iframeListener.sendFrozenGameStateEvent({ - roomId:this.RoomId, - data: this.mapFile + mapUrl: this.MapUrlFile, + nickName: this.playerName, + startLayerName: this.startLayerName, + uuid: localUserStore.getLocalUser()?.uuid, + roomId: this.RoomId, + data: this.mapFile, + players: playerObject }) })); From ffe03d40f5691c4114269916d68bbff78ba98c7b Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 00:27:21 +0200 Subject: [PATCH 08/87] option to update tile # Conflicts: # front/src/Api/Events/ApiUpdateTileEvent.ts # front/src/Api/IframeListener.ts # front/src/Phaser/Game/GameScene.ts --- front/src/Api/Events/ApiUpdateTileEvent.ts | 16 + front/src/Api/IframeListener.ts | 7 + front/src/Phaser/Game/GameScene.ts | 346 ++++++++++++--------- front/src/Phaser/Map/ITiledMap.ts | 21 +- 4 files changed, 227 insertions(+), 163 deletions(-) create mode 100644 front/src/Api/Events/ApiUpdateTileEvent.ts diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ApiUpdateTileEvent.ts new file mode 100644 index 00000000..8a53fbe5 --- /dev/null +++ b/front/src/Api/Events/ApiUpdateTileEvent.ts @@ -0,0 +1,16 @@ + +import * as tg from "generic-type-guard"; +export const updateTile = "updateTile" + + +export const isUpdateTileEvent = + new tg.IsInterface().withProperties({ + x: tg.isNumber, + y: tg.isNumber, + tile: tg.isUnion(tg.isNumber, tg.isString), + layer: tg.isUnion(tg.isNumber, tg.isString) + }).get(); +/** + * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. + */ +export type UpdateTileEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index f20e055c..715eddc0 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -57,6 +57,9 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); + private readonly _updateTileEvent: Subject = new Subject(); + public readonly updateTileEvent = this._updateTileEvent.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -110,6 +113,10 @@ class IframeListener { this._removeBubbleStream.next(); }else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){ this._loadPageStream.next(payload.data.url); + } else if (payload.type == "getState") { + this._gameStateStream.next(); + } else if (payload.type == "updateTile" && isUpdateTileEvent(payload.data)) { + this._updateTileEvent.next(payload.data) } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7c48239b..138ca5ae 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,4 @@ -import {gameManager, HasMovedEvent} from "./GameManager"; +import { gameManager, HasMovedEvent } from "./GameManager"; import { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -9,7 +9,7 @@ import { PositionInterface, RoomJoinedMessageInterface } from "../../Connexion/ConnexionModels"; -import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; +import { CurrentGamerInterface, hasMovedEventName, Player } from "../Player/Player"; import { DEBUG_MODE, JITSI_PRIVATE_MODE, @@ -27,15 +27,15 @@ import { ITiledMapTileLayer, ITiledTileSet } from "../Map/ITiledMap"; -import {AddPlayerInterface} from "./AddPlayerInterface"; -import {PlayerAnimationDirections} from "../Player/Animation"; -import {PlayerMovement} from "./PlayerMovement"; -import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; -import {RemotePlayer} from "../Entity/RemotePlayer"; -import {Queue} from 'queue-typescript'; -import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; -import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import {lazyLoadPlayerCharacterTextures, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; +import { AddPlayerInterface } from "./AddPlayerInterface"; +import { PlayerAnimationDirections } from "../Player/Animation"; +import { PlayerMovement } from "./PlayerMovement"; +import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; +import { RemotePlayer } from "../Entity/RemotePlayer"; +import { Queue } from 'queue-typescript'; +import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer"; +import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; +import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { CenterListener, JITSI_MESSAGE_PROPERTIES, @@ -48,52 +48,56 @@ import { AUDIO_VOLUME_PROPERTY, AUDIO_LOOP_PROPERTY } from "../../WebRtc/LayoutManager"; -import {GameMap} from "./GameMap"; -import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; -import {mediaManager} from "../../WebRtc/MediaManager"; -import {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; -import {ActionableItem} from "../Items/ActionableItem"; -import {UserInputManager} from "../UserInput/UserInputManager"; -import {UserMovedMessage} from "../../Messages/generated/messages_pb"; -import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; -import {connectionManager} from "../../Connexion/ConnectionManager"; -import {RoomConnection} from "../../Connexion/RoomConnection"; -import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; -import {userMessageManager} from "../../Administration/UserMessageManager"; -import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; -import {ResizableScene} from "../Login/ResizableScene"; -import {Room} from "../../Connexion/Room"; -import {jitsiFactory} from "../../WebRtc/JitsiFactory"; -import {urlManager} from "../../Url/UrlManager"; -import {audioManager} from "../../WebRtc/AudioManager"; -import {PresentationModeIcon} from "../Components/PresentationModeIcon"; -import {ChatModeIcon} from "../Components/ChatModeIcon"; -import {OpenChatIcon, openChatIconName} from "../Components/OpenChatIcon"; -import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; -import {TextureError} from "../../Exception/TextureError"; -import {addLoader} from "../Components/Loader"; -import {ErrorSceneName} from "../Reconnecting/ErrorScene"; -import {localUserStore} from "../../Connexion/LocalUserStore"; -import {iframeListener} from "../../Api/IframeListener"; -import {HtmlUtils} from "../../WebRtc/HtmlUtils"; +import { GameMap } from "./GameMap"; +import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; +import { ActionableItem } from "../Items/ActionableItem"; +import { UserInputManager } from "../UserInput/UserInputManager"; +import { UserMovedMessage } from "../../Messages/generated/messages_pb"; +import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; +import { connectionManager } from "../../Connexion/ConnectionManager"; +import { RoomConnection } from "../../Connexion/RoomConnection"; +import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; +import { userMessageManager } from "../../Administration/UserMessageManager"; +import { ConsoleGlobalMessageManager } from "../../Administration/ConsoleGlobalMessageManager"; +import { ResizableScene } from "../Login/ResizableScene"; +import { Room } from "../../Connexion/Room"; +import { jitsiFactory } from "../../WebRtc/JitsiFactory"; +import { urlManager } from "../../Url/UrlManager"; +import { audioManager } from "../../WebRtc/AudioManager"; +import { PresentationModeIcon } from "../Components/PresentationModeIcon"; +import { ChatModeIcon } from "../Components/ChatModeIcon"; +import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; +import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; +import { TextureError } from "../../Exception/TextureError"; +import { addLoader } from "../Components/Loader"; +import { ErrorSceneName } from "../Reconnecting/ErrorScene"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { iframeListener } from "../../Api/IframeListener"; +import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; import GameObject = Phaser.GameObjects.GameObject; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import DOMElement = Phaser.GameObjects.DOMElement; -import EVENT_TYPE =Phaser.Scenes.Events -import {Subscription} from "rxjs"; -import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; +import EVENT_TYPE = Phaser.Scenes.Events +import { Subscription } from "rxjs"; +import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import {TextUtils} from "../Components/TextUtils"; import {LayersIterator} from "../Map/LayersIterator"; import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; +import { TextUtils } from "../Components/TextUtils"; +import { touchScreenManager } from "../../Touch/TouchScreenManager"; +import { PinchManager } from "../UserInput/PinchManager"; +import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; export interface GameSceneInitInterface { - initPosition: PointInterface|null, + initPosition: PointInterface | null, reconnecting: boolean } @@ -130,10 +134,10 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; export class GameScene extends ResizableScene implements CenterListener { - Terrains : Array; + Terrains: Array; CurrentPlayer!: CurrentGamerInterface; MapPlayers!: Phaser.Physics.Arcade.Group; - MapPlayersByKey : Map = new Map(); + MapPlayersByKey: Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; Layers!: Array; Objects!: Array; @@ -143,10 +147,10 @@ export class GameScene extends ResizableScene implements CenterListener { startY!: number; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; - pendingEvents: Queue = new Queue(); - private initPosition: PositionInterface|null = null; + pendingEvents: Queue = new Queue(); + private initPosition: PositionInterface | null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); - public connection: RoomConnection|undefined; + public connection: RoomConnection | undefined; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; @@ -155,7 +159,7 @@ export class GameScene extends ResizableScene implements CenterListener { // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; - private iframeSubscriptionList! : Array; + private iframeSubscriptionList!: Array; MapUrlFile: string; RoomId: string; instance: string; @@ -174,19 +178,19 @@ export class GameScene extends ResizableScene implements CenterListener { private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. - private outlinedItem: ActionableItem|null = null; + private outlinedItem: ActionableItem | null = null; public userInputManager!: UserInputManager; - private isReconnecting: boolean|undefined = undefined; + private isReconnecting: boolean | undefined = undefined; private startLayerName!: string | null; private openChatIcon!: OpenChatIcon; private playerName!: string; private characterLayers!: string[]; - private companion!: string|null; - private messageSubscription: Subscription|null = null; - private popUpElements : Map = new Map(); - private originalMapUrl: string|undefined; + private companion!: string | null; + private messageSubscription: Subscription | null = null; + private popUpElements: Map = new Map(); + private originalMapUrl: string | undefined; - constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { + constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ key: customKey ?? room.id }); @@ -222,13 +226,13 @@ export class GameScene extends ResizableScene implements CenterListener { this.load.image(joystickBaseKey, joystickBaseImg); this.load.image(joystickThumbKey, joystickThumbImg); } - this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { + this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { this.originalMapUrl = this.MapUrlFile; this.MapUrlFile = this.MapUrlFile.replace('http://', 'https://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); return; @@ -242,7 +246,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.originalMapUrl = this.MapUrlFile; this.MapUrlFile = this.MapUrlFile.replace('https://', 'http://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); return; @@ -254,7 +258,7 @@ export class GameScene extends ResizableScene implements CenterListener { message: this.originalMapUrl ?? file.src }); }); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (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 @@ -266,7 +270,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.onMapLoad(data); } - this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32}); + this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', { frameWidth: 32, frameHeight: 32 }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); } @@ -292,7 +296,7 @@ export class GameScene extends ResizableScene implements CenterListener { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup') { for (const object of layer.objects) { - let objectsOfType: ITiledMapObject[]|undefined; + let objectsOfType: ITiledMapObject[] | undefined; if (!objects.has(object.type)) { objectsOfType = new Array(); } else { @@ -320,7 +324,7 @@ export class GameScene extends ResizableScene implements CenterListener { } default: continue; - //throw new Error('Unsupported object type: "'+ itemType +'"'); + //throw new Error('Unsupported object type: "'+ itemType +'"'); } itemFactory.preload(this.load); @@ -355,7 +359,7 @@ export class GameScene extends ResizableScene implements CenterListener { } //hook initialisation - init(initData : GameSceneInitInterface) { + init(initData: GameSceneInitInterface) { if (initData.initPosition !== undefined) { this.initPosition = initData.initPosition; //todo: still used? } @@ -433,7 +437,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.Objects = new Array(); //initialise list of other player - this.MapPlayers = this.physics.add.group({immovable: true}); + this.MapPlayers = this.physics.add.group({ immovable: true }); //create input to move @@ -522,7 +526,7 @@ export class GameScene extends ResizableScene implements CenterListener { bottom: camera.scrollY + camera.height, }, this.companion - ).then((onConnect: OnConnectInterface) => { + ).then((onConnect: OnConnectInterface) => { this.connection = onConnect.connection; this.connection.onUserJoins((message: MessageUserJoined) => { @@ -673,23 +677,23 @@ export class GameScene extends ResizableScene implements CenterListener { const contextRed = this.circleRedTexture.context; contextRed.beginPath(); contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); - //context.lineWidth = 5; + //context.lineWidth = 5; contextRed.strokeStyle = '#ff0000'; contextRed.stroke(); this.circleRedTexture.refresh(); } - private safeParseJSONstring(jsonString: string|undefined, propertyName: string) { + private safeParseJSONstring(jsonString: string | undefined, propertyName: string) { try { return jsonString ? JSON.parse(jsonString) : {}; - } catch(e) { + } catch (e) { console.warn('Invalid JSON found in property "' + propertyName + '" of the map:' + jsonString, e); return {} } } - private triggerOnMapLayerPropertyChange(){ + private triggerOnMapLayerPropertyChange() { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); @@ -700,22 +704,22 @@ export class GameScene extends ResizableScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('openWebsite', this.userInputManager); coWebsiteManager.closeCoWebsite(); - }else{ + } else { const openWebsiteFunction = () => { coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); - if(openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); - if(message === undefined){ + if (message === undefined) { message = 'Press SPACE or touch here to open web site'; } layoutManager.addActionButton('openWebsite', message.toString(), () => { openWebsiteFunction(); }, this.userInputManager); - }else{ + } else { openWebsiteFunction(); } } @@ -724,12 +728,12 @@ export class GameScene extends ResizableScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('jitsiRoom', this.userInputManager); this.stopJitsi(); - }else{ + } else { const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); - const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; + const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; if (JITSI_PRIVATE_MODE && !jitsiUrl) { - const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; + const adminTag = allProps.get("jitsiRoomAdminTag") as string | undefined; this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); } else { @@ -739,7 +743,7 @@ export class GameScene extends ResizableScene implements CenterListener { } const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); - if(jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(JITSI_MESSAGE_PROPERTIES); if (message === undefined) { message = 'Press SPACE or touch here to enter Jitsi Meet room'; @@ -747,7 +751,7 @@ export class GameScene extends ResizableScene implements CenterListener { layoutManager.addActionButton('jitsiRoom', message.toString(), () => { openJitsiRoomFunction(); }, this.userInputManager); - }else{ + } else { openJitsiRoomFunction(); } } @@ -760,8 +764,8 @@ export class GameScene extends ResizableScene implements CenterListener { } }); this.gameMap.onPropertyChange('playAudio', (newValue, oldValue, allProps) => { - const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number|undefined; - const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean|undefined; + const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined; + const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined; newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop); }); // TODO: This legacy property should be removed at some point @@ -780,13 +784,13 @@ export class GameScene extends ResizableScene implements CenterListener { } private listenToIframeEvents(): void { - this.iframeSubscriptionList = []; - this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { + this.iframeSubscriptionList = []; + this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { - let objectLayerSquare : ITiledMapObject; + let objectLayerSquare: ITiledMapObject; const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject); - if (targetObjectData !== undefined){ - objectLayerSquare = targetObjectData; + if (targetObjectData !== undefined) { + objectLayerSquare = targetObjectData; } else { console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map."); return; @@ -799,14 +803,14 @@ ${escapedMessage} html += buttonContainer; let id = 0; for (const button of openPopupEvent.buttons) { - html += ``; + html += ``; id++; } html += ''; - const domElement = this.add.dom(objectLayerSquare.x , + const domElement = this.add.dom(objectLayerSquare.x, objectLayerSquare.y).createFromHTML(html); - const container : HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement; + const container: HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement; container.style.width = objectLayerSquare.width + "px"; domElement.scale = 0; domElement.setClassName('popUpElement'); @@ -826,67 +830,99 @@ ${escapedMessage} id++; } this.tweens.add({ - targets : domElement , - scale : 1, - ease : "EaseOut", - duration : 400, + targets: domElement, + scale: 1, + ease: "EaseOut", + duration: 400, }); this.popUpElements.set(openPopupEvent.popupId, domElement); })); - this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { + this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { const popUpElement = this.popUpElements.get(closePopupEvent.popupId); if (popUpElement === undefined) { - console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?'); + console.error('Could not close popup with ID ', closePopupEvent.popupId, '. Maybe it has already been closed?'); } this.tweens.add({ - targets : popUpElement , - scale : 0, - ease : "EaseOut", - duration : 400, - onComplete : () => { + targets: popUpElement, + scale: 0, + ease: "EaseOut", + duration: 400, + onComplete: () => { popUpElement?.destroy(); this.popUpElements.delete(closePopupEvent.popupId); }, }); })); - this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(() => { this.userInputManager.disableControls(); })); - this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(() => { this.userInputManager.restoreControls(); })); - this.iframeSubscriptionList.push(iframeListener.loadPageStream.subscribe((url:string)=>{ - this.loadNextGame(url).then(()=>{ - this.events.once(EVENT_TYPE.POST_UPDATE,()=>{ + this.iframeSubscriptionList.push(iframeListener.loadPageStream.subscribe((url: string) => { + this.loadNextGame(url).then(() => { + this.events.once(EVENT_TYPE.POST_UPDATE, () => { this.onMapExit(url); }) }) })); - let scriptedBubbleSprite : Sprite; - this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ - scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); + + this.iframeSubscriptionList.push(iframeListener.updateTileEvent.subscribe(event => { + const layer = this.Layers.find(layer => layer.layer.name == event.layer) + if (layer) { + const tile = layer.getTileAt(event.x, event.y) + if (typeof event.tile == "string") { + const tileIndex = this.getIndexForTileType(event.tile); + if (tileIndex) { + tile.index = tileIndex + } else { + return + } + } else { + tile.index = event.tile + } + this.scene.scene.sys.game.events.emit("contextrestored") + } + })) + + let scriptedBubbleSprite: Sprite; + this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(() => { + scriptedBubbleSprite = new Sprite(this, this.CurrentPlayer.x + 25, this.CurrentPlayer.y, 'circleSprite-white'); scriptedBubbleSprite.setDisplayOrigin(48, 48); this.add.existing(scriptedBubbleSprite); })); - this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(() => { scriptedBubbleSprite.destroy(); })); } + private getIndexForTileType(tileType: string): number | undefined { + for (const tileset of this.mapFile.tilesets) { + if (tileset.tiles) { + for (const tilesetTile of tileset.tiles) { + if (tilesetTile.type == tileType) { + return tileset.firstgid + tilesetTile.id + } + } + } + } + return undefined + } + private getMapDirUrl(): string { return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); } private onMapExit(exitKey: string) { - const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); - if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); + const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); + if (!roomId) throw new Error('Could not find the room from its exit key: ' + exitKey); urlManager.pushStartLayerNameToUrl(hash); if (roomId !== this.scene.key) { if (this.scene.get(roomId) === null) { @@ -922,7 +958,7 @@ ${escapedMessage} this.simplePeer?.unregister(); this.messageSubscription?.unsubscribe(); - for(const iframeEvents of this.iframeSubscriptionList){ + for (const iframeEvents of this.iframeSubscriptionList) { iframeEvents.unsubscribe(); } } @@ -942,7 +978,7 @@ ${escapedMessage} private switchLayoutMode(): void { //if discussion is activated, this layout cannot be activated - if(mediaManager.activatedDiscussion){ + if (mediaManager.activatedDiscussion) { return; } const mode = layoutManager.getLayoutMode(); @@ -983,24 +1019,24 @@ ${escapedMessage} private initPositionFromLayerName(layerName: string) { for (const layer of this.gameMap.layersIterator) { - if ((layerName === layer.name || layer.name.endsWith('/'+layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { + if ((layerName === layer.name || layer.name.endsWith('/' + layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { const startPosition = this.startUser(layer); - this.startX = startPosition.x + this.mapFile.tilewidth/2; - this.startY = startPosition.y + this.mapFile.tileheight/2; + this.startX = startPosition.x + this.mapFile.tilewidth / 2; + this.startY = startPosition.y + this.mapFile.tileheight / 2; } } } - private getExitUrl(layer: ITiledMapLayer): string|undefined { - return this.getProperty(layer, "exitUrl") as string|undefined; + private getExitUrl(layer: ITiledMapLayer): string | undefined { + 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 getExitSceneUrl(layer: ITiledMapLayer): string | undefined { + return this.getProperty(layer, "exitSceneUrl") as string | undefined; } private isStartLayer(layer: ITiledMapLayer): boolean { @@ -1011,8 +1047,8 @@ ${escapedMessage} return (this.getProperties(map, "script") as string[]).map((script) => (new URL(script, this.MapUrlFile)).toString()); } - private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined { - const properties: ITiledMapLayerProperty[]|undefined = layer.properties; + private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined { + const properties: ITiledMapLayerProperty[] | undefined = layer.properties; if (!properties) { return undefined; } @@ -1023,8 +1059,8 @@ ${escapedMessage} return obj.value; } - private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { - const properties: ITiledMapLayerProperty[]|undefined = layer.properties; + private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] { + const properties: ITiledMapLayerProperty[] | undefined = layer.properties; if (!properties) { return []; } @@ -1032,30 +1068,30 @@ ${escapedMessage} } //todo: push that into the gameManager - private async loadNextGame(exitSceneIdentifier: string){ - const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); + private async loadNextGame(exitSceneIdentifier: string) { + const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const room = new Room(roomId); await gameManager.loadMap(room, this.scene); } private startUser(layer: ITiledMapTileLayer): PositionInterface { const tiles = layer.data; - if (typeof(tiles) === 'string') { + if (typeof (tiles) === 'string') { throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); } - const possibleStartPositions : PositionInterface[] = []; - tiles.forEach((objectKey : number, key: number) => { - if(objectKey === 0){ + const possibleStartPositions: PositionInterface[] = []; + tiles.forEach((objectKey: number, key: number) => { + if (objectKey === 0) { return; } const y = Math.floor(key / layer.width); const x = key % layer.width; - possibleStartPositions.push({x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth}); + possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth }); }); // Get a value at random amongst allowed values if (possibleStartPositions.length === 0) { - console.warn('The start layer "'+layer.name+'" for this map is empty.'); + console.warn('The start layer "' + layer.name + '" for this map is empty.'); return { x: 0, y: 0 @@ -1067,12 +1103,12 @@ ${escapedMessage} //todo: in a dedicated class/function? initCamera() { - this.cameras.main.setBounds(0,0, this.Map.widthInPixels, this.Map.heightInPixels); + this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.updateCameraOffset(); this.cameras.main.setZoom(ZOOM_LEVEL); } - addLayer(Layer : Phaser.Tilemaps.StaticTilemapLayer){ + addLayer(Layer: Phaser.Tilemaps.StaticTilemapLayer) { this.Layers.push(Layer); } @@ -1082,7 +1118,7 @@ ${escapedMessage} this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - Layer.setCollisionByProperty({collides: true}); + Layer.setCollisionByProperty({ collides: true }); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer Layer.renderDebug(this.add.graphics(), { @@ -1094,7 +1130,7 @@ ${escapedMessage} }); } - createCurrentPlayer(){ + createCurrentPlayer() { //TODO create animation moving between exit and start const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers); try { @@ -1110,8 +1146,8 @@ ${escapedMessage} this.companion, this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined ); - }catch (err){ - if(err instanceof TextureError) { + } catch (err) { + if (err instanceof TextureError) { gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); } throw err; @@ -1172,7 +1208,7 @@ ${escapedMessage} } let shortestDistance: number = Infinity; - let selectedItem: ActionableItem|null = null; + let selectedItem: ActionableItem | null = null; for (const item of this.actionableItems.values()) { const distance = item.actionableDistance(x, y); if (distance !== null && distance < shortestDistance) { @@ -1206,7 +1242,7 @@ ${escapedMessage} * @param time * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ - update(time: number, delta: number) : void { + update(time: number, delta: number): void { mediaManager.setLastUpdateScene(); this.currentTick = time; this.CurrentPlayer.moveUser(delta); @@ -1263,8 +1299,8 @@ ${escapedMessage} const currentPlayerId = this.connection?.getUserId(); this.removeAllRemotePlayers(); // load map - usersPosition.forEach((userPosition : MessageUserPositionInterface) => { - if(userPosition.userId === currentPlayerId){ + usersPosition.forEach((userPosition: MessageUserPositionInterface) => { + if (userPosition.userId === currentPlayerId) { return; } this.addPlayer(userPosition); @@ -1274,16 +1310,16 @@ ${escapedMessage} /** * Called by the connexion when a new player arrives on a map */ - public addPlayer(addPlayerData : AddPlayerInterface) : void { + public addPlayer(addPlayerData: AddPlayerInterface): void { this.pendingEvents.enqueue({ type: "AddPlayerEvent", event: addPlayerData }); } - private doAddPlayer(addPlayerData : AddPlayerInterface): void { + private doAddPlayer(addPlayerData: AddPlayerInterface): void { //check if exist player, if exist, move position - if(this.MapPlayersByKey.has(addPlayerData.userId)){ + if (this.MapPlayersByKey.has(addPlayerData.userId)) { this.updatePlayerPosition({ userId: addPlayerData.userId, position: addPlayerData.position @@ -1344,10 +1380,10 @@ ${escapedMessage} } private doUpdatePlayerPosition(message: MessageUserMovedInterface): void { - const player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); + const player: RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); if (player === undefined) { //throw new Error('Cannot find player with ID "' + message.userId +'"'); - console.error('Cannot update position of player with ID "' + message.userId +'": player not found'); + console.error('Cannot update position of player with ID "' + message.userId + '": player not found'); return; } @@ -1391,7 +1427,7 @@ ${escapedMessage} doDeleteGroup(groupId: number): void { const group = this.groups.get(groupId); - if(!group){ + if (!group) { return; } group.destroy(); @@ -1419,7 +1455,7 @@ ${escapedMessage} bottom: camera.scrollY + camera.height, }); } - private getObjectLayerData(objectName : string) : ITiledMapObject| undefined{ + private getObjectLayerData(objectName: string): ITiledMapObject | undefined { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { for (const object of layer.objects) { @@ -1454,7 +1490,7 @@ ${escapedMessage} xCenter /= ZOOM_LEVEL * RESOLUTION; yCenter /= ZOOM_LEVEL * RESOLUTION; - this.cameras.main.startFollow(this.CurrentPlayer, true, 1, 1, xCenter - this.game.renderer.width / 2, yCenter - this.game.renderer.height / 2); + this.cameras.main.startFollow(this.CurrentPlayer, true, 1, 1, xCenter - this.game.renderer.width / 2, yCenter - this.game.renderer.height / 2); } public onCenterChange(): void { @@ -1463,16 +1499,16 @@ ${escapedMessage} public startJitsi(roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); - const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string|undefined, 'jitsiConfig'); - const jitsiInterfaceConfig = this.safeParseJSONstring(allProps.get("jitsiInterfaceConfig") as string|undefined, 'jitsiInterfaceConfig'); - const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; + const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string | undefined, 'jitsiConfig'); + const jitsiInterfaceConfig = this.safeParseJSONstring(allProps.get("jitsiInterfaceConfig") as string | undefined, 'jitsiInterfaceConfig'); + const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl); this.connection?.setSilent(true); mediaManager.hideGameOverlay(); //permit to stop jitsi when user close iframe - mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => { + mediaManager.addTriggerCloseJitsiFrameButton('close-jisi', () => { this.stopJitsi(); }); } @@ -1486,7 +1522,7 @@ ${escapedMessage} } //todo: put this into an 'orchestrator' scene (EntryScene?) - private bannedUser(){ + private bannedUser() { this.cleanupClosingScene(); this.userInputManager.disableControls(); this.scene.start(ErrorSceneName, { diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index c4828911..27fe9f45 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -34,7 +34,7 @@ export interface ITiledMap { export interface ITiledMapLayerProperty { name: string; type: string; - value: string|boolean|number|undefined; + value: string | boolean | number | undefined; } /*export interface ITiledMapLayerBooleanProperty { @@ -63,7 +63,7 @@ export interface ITiledMapGroupLayer { export interface ITiledMapTileLayer { id?: number, - data: number[]|string; + data: number[] | string; height: number; name: string; opacity: number; @@ -114,7 +114,7 @@ export interface ITiledMapObject { gid: number; height: number; name: string; - properties: {[key: string]: string}; + properties: { [key: string]: string }; rotation: number; type: string; visible: boolean; @@ -130,12 +130,12 @@ export interface ITiledMapObject { /** * Polygon points */ - polygon: {x: number, y: number}[]; + polygon: { x: number, y: number }[]; /** * Polyline points */ - polyline: {x: number, y: number}[]; + polyline: { x: number, y: number }[]; text?: ITiledText } @@ -149,7 +149,7 @@ export interface ITiledText { underline?: boolean, italic?: boolean, strikeout?: boolean, - halign?: "center"|"right"|"justify"|"left" + halign?: "center" | "right" | "justify" | "left" } export interface ITiledTileSet { @@ -160,14 +160,14 @@ export interface ITiledTileSet { imagewidth: number; margin: number; name: string; - properties: {[key: string]: string}; + properties: { [key: string]: string }; spacing: number; tilecount: number; tileheight: number; tilewidth: number; transparentcolor: string; terrains: ITiledMapTerrain[]; - tiles: {[key: string]: { terrain: number[] }}; + tiles: Array; /** * Refers to external tileset file (should be JSON) @@ -175,6 +175,11 @@ export interface ITiledTileSet { source: string; } +export interface ITile { + id: number, + type?: string +} + export interface ITiledMapTerrain { name: string; tile: number; From bed45a831031f99c2af5d5fb7f11a4c773814dca Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 00:31:54 +0200 Subject: [PATCH 09/87] cherry pick conflicts --- front/src/Api/IframeListener.ts | 59 +++++++++++++++--------------- front/src/Phaser/Game/GameScene.ts | 5 --- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 715eddc0..f97e80ae 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,18 +1,19 @@ -import {Subject} from "rxjs"; -import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; -import {IframeEvent, isIframeEventWrapper} from "./Events/IframeEvent"; -import {UserInputChatEvent} from "./Events/UserInputChatEvent"; +import { Subject } from "rxjs"; +import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; +import { IframeEvent, isIframeEventWrapper } from "./Events/IframeEvent"; +import { UserInputChatEvent } from "./Events/UserInputChatEvent"; import * as crypto from "crypto"; -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; -import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; -import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; -import {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; -import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; -import {scriptUtils} from "./ScriptUtils"; -import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; -import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { HtmlUtils } from "../WebRtc/HtmlUtils"; +import { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; +import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; +import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; +import { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; +import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; +import { scriptUtils } from "./ScriptUtils"; +import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; +import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { isLoadPageEvent } from './Events/LoadPageEvent'; +import { isUpdateTileEvent, UpdateTileEvent } from './Events/ApiUpdateTileEvent'; /** @@ -32,7 +33,7 @@ class IframeListener { private readonly _goToPageStream: Subject = new Subject(); public readonly goToPageStream = this._goToPageStream.asObservable(); - + private readonly _loadPageStream: Subject = new Subject(); public readonly loadPageStream = this._loadPageStream.asObservable(); @@ -88,33 +89,31 @@ class IframeListener { } else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) { this._closePopupStream.next(payload.data); } - else if(payload.type === 'openTab' && isOpenTabEvent(payload.data)) { + else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) { scriptUtils.openTab(payload.data.url); } - else if(payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { + else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { scriptUtils.goToPage(payload.data.url); } - else if(payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) { + else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) { scriptUtils.openCoWebsite(payload.data.url); } - else if(payload.type === 'closeCoWebSite') { + else if (payload.type === 'closeCoWebSite') { scriptUtils.closeCoWebSite(); } - else if (payload.type === 'disablePlayerControl'){ + else if (payload.type === 'disablePlayerControl') { this._disablePlayerControlStream.next(); } - else if (payload.type === 'restorePlayerControl'){ + else if (payload.type === 'restorePlayerControl') { this._enablePlayerControlStream.next(); } - else if (payload.type === 'displayBubble'){ + else if (payload.type === 'displayBubble') { this._displayBubbleStream.next(); } - else if (payload.type === 'removeBubble'){ + else if (payload.type === 'removeBubble') { this._removeBubbleStream.next(); - }else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){ + } else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { this._loadPageStream.next(payload.data.url); - } else if (payload.type == "getState") { - this._gameStateStream.next(); } else if (payload.type == "updateTile" && isUpdateTileEvent(payload.data)) { this._updateTileEvent.next(payload.data) } @@ -144,7 +143,7 @@ class IframeListener { const iframe = document.createElement('iframe'); iframe.id = this.getIFrameId(scriptUrl); iframe.style.display = 'none'; - iframe.src = '/iframe.html?script='+encodeURIComponent(scriptUrl); + iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl); // We are putting a sandbox on this script because it will run in the same domain as the main website. iframe.sandbox.add('allow-scripts'); @@ -168,8 +167,8 @@ class IframeListener { '\n' + '\n' + '\n' + - '\n' + - '\n' + + '\n' + + '\n' + '\n' + '\n'; @@ -186,14 +185,14 @@ class IframeListener { } private getIFrameId(scriptUrl: string): string { - return 'script'+crypto.createHash('md5').update(scriptUrl).digest("hex"); + return 'script' + crypto.createHash('md5').update(scriptUrl).digest("hex"); } unregisterScript(scriptUrl: string): void { const iFrameId = this.getIFrameId(scriptUrl); const iframe = HtmlUtils.getElementByIdOrFail(iFrameId); if (!iframe) { - throw new Error('Unknown iframe for script "'+scriptUrl+'"'); + throw new Error('Unknown iframe for script "' + scriptUrl + '"'); } this.unregisterIframe(iframe); iframe.remove(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 138ca5ae..9ed86716 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -86,11 +86,6 @@ import EVENT_TYPE = Phaser.Scenes.Events import { Subscription } from "rxjs"; import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; -import {TextUtils} from "../Components/TextUtils"; -import {LayersIterator} from "../Map/LayersIterator"; -import {touchScreenManager} from "../../Touch/TouchScreenManager"; -import {PinchManager} from "../UserInput/PinchManager"; -import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import { TextUtils } from "../Components/TextUtils"; import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { PinchManager } from "../UserInput/PinchManager"; From 8db72d2dfd4e707fa07e982d4afb549bc286303c Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 01:21:37 +0200 Subject: [PATCH 10/87] refactored to Array of tile --- front/src/Api/Events/ApiUpdateTileEvent.ts | 5 +++-- front/src/Phaser/Game/GameScene.ts | 26 ++++++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ApiUpdateTileEvent.ts index 8a53fbe5..094596a4 100644 --- a/front/src/Api/Events/ApiUpdateTileEvent.ts +++ b/front/src/Api/Events/ApiUpdateTileEvent.ts @@ -3,13 +3,14 @@ import * as tg from "generic-type-guard"; export const updateTile = "updateTile" -export const isUpdateTileEvent = +export const isUpdateTileEvent = tg.isArray( new tg.IsInterface().withProperties({ x: tg.isNumber, y: tg.isNumber, tile: tg.isUnion(tg.isNumber, tg.isString), layer: tg.isUnion(tg.isNumber, tg.isString) - }).get(); + }).get() +); /** * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. */ diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9ed86716..5687c7e5 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -868,18 +868,20 @@ ${escapedMessage} })); this.iframeSubscriptionList.push(iframeListener.updateTileEvent.subscribe(event => { - const layer = this.Layers.find(layer => layer.layer.name == event.layer) - if (layer) { - const tile = layer.getTileAt(event.x, event.y) - if (typeof event.tile == "string") { - const tileIndex = this.getIndexForTileType(event.tile); - if (tileIndex) { - tile.index = tileIndex + for (const eventTile of event) { + const layer = this.Layers.find(layer => layer.layer.name == eventTile.layer) + if (layer) { + const tile = layer.getTileAt(eventTile.x, eventTile.y) + if (typeof eventTile.tile == "string") { + const tileIndex = this.getIndexForTileType(eventTile.tile); + if (tileIndex) { + tile.index = tileIndex + } else { + return + } } else { - return + tile.index = eventTile.tile } - } else { - tile.index = event.tile } this.scene.scene.sys.game.events.emit("contextrestored") } @@ -898,7 +900,7 @@ ${escapedMessage} } - private getIndexForTileType(tileType: string): number | undefined { + private getIndexForTileType(tileType: string): number | null { for (const tileset of this.mapFile.tilesets) { if (tileset.tiles) { for (const tilesetTile of tileset.tiles) { @@ -908,7 +910,7 @@ ${escapedMessage} } } } - return undefined + return null } private getMapDirUrl(): string { From 46996f70497666bf79f5f3dde624252634eb3ae9 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 01:27:17 +0200 Subject: [PATCH 11/87] moved event trigger out of index array --- front/src/Phaser/Game/GameScene.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5687c7e5..674087e0 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -883,8 +883,8 @@ ${escapedMessage} tile.index = eventTile.tile } } - this.scene.scene.sys.game.events.emit("contextrestored") } + this.scene.scene.sys.game.events.emit("contextrestored") })) let scriptedBubbleSprite: Sprite; From a6ba8d41b9a9c7d73cec0452b313c34bfd9e38b4 Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 11:19:18 +0200 Subject: [PATCH 12/87] implement show/hide layer with scripting --- front/src/Api/Events/LayerEvent.ts | 10 ++++++++++ front/src/Api/IframeListener.ts | 15 ++++++++++++++- front/src/iframe_api.ts | 21 ++++++++++++++++++++ maps/tests/iframe.html | 31 +++++++++++++++++++++++++++++- maps/tests/iframe_api.json | 25 +++++++++++++++++++++--- 5 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 front/src/Api/Events/LayerEvent.ts diff --git a/front/src/Api/Events/LayerEvent.ts b/front/src/Api/Events/LayerEvent.ts new file mode 100644 index 00000000..f854248b --- /dev/null +++ b/front/src/Api/Events/LayerEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isLayerEvent = + new tg.IsInterface().withProperties({ + name: tg.isString, + }).get(); +/** + * A message sent from the iFrame to the game to show/hide a layer. + */ +export type LayerEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 7e51a281..0820785a 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,6 +12,7 @@ import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import {scriptUtils} from "./ScriptUtils"; import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; /** @@ -52,6 +53,12 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); + private readonly _showLayerStream: Subject = new Subject(); + public readonly showLayerStream = this._showLayerStream.asObservable(); + + private readonly _hideLayerStream: Subject = new Subject(); + public readonly hideLayerStream = this._hideLayerStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -73,7 +80,13 @@ class IframeListener { const payload = message.data; if (isIframeEventWrapper(payload)) { - if (payload.type === 'chat' && isChatEvent(payload.data)) { + if (payload.type ==='showLayer' && isLayerEvent(payload.data)) { + console.log('showLayer 2'); + this._showLayerStream.next(payload.data); + } else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) { + console.log('hideLayer 2'); + this._hideLayerStream.next(payload.data); + } else if (payload.type === 'chat' && isChatEvent(payload.data)) { this._chatStream.next(payload.data); } else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) { this._openPopupStream.next(payload.data); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 18d8d172..0b9fac46 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,6 +9,7 @@ import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent"; import {OpenTabEvent} from "./Api/Events/OpenTabEvent"; import {GoToPageEvent} from "./Api/Events/GoToPageEvent"; import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent"; +import {LayerEvent} from "./Api/Events/LayerEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -24,6 +25,8 @@ interface WorkAdventureApi { restorePlayerControl() : void; displayBubble() : void; removeBubble() : void; + showLayer(layer: string) : void; + hideLayer(layer: string) : void; } declare global { @@ -88,6 +91,24 @@ window.WA = { } as ChatEvent }, '*'); }, + showLayer(layer: string) : void { + console.log('showLayer'); + window.parent.postMessage({ + 'type' : 'showLayer', + 'data' : { + 'name' : layer + } as LayerEvent + }, '*'); + }, + hideLayer(layer: string) : void { + console.log('hideLayer'); + window.parent.postMessage({ + 'type' : 'hideLayer', + 'data' : { + 'name' : layer + } as LayerEvent + }, '*'); + }, disablePlayerControl() : void { window.parent.postMessage({'type' : 'disablePlayerControl'},'*'); }, diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index 23bfb479..4c7cd044 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -3,7 +3,7 @@ @@ -21,5 +21,34 @@ document.getElementById('chatSent').append(chatDiv); })); +
+ +
+ + diff --git a/maps/tests/iframe_api.json b/maps/tests/iframe_api.json index fa138500..db840b3f 100644 --- a/maps/tests/iframe_api.json +++ b/maps/tests/iframe_api.json @@ -1,4 +1,11 @@ { "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, "height":10, "infinite":false, "layers":[ @@ -49,6 +56,18 @@ "x":0, "y":0 }, + { + "data":[0, 0, 93, 0, 104, 0, 0, 0, 0, 0, 0, 0, 104, 0, 115, 0, 0, 0, 93, 0, 0, 0, 115, 0, 0, 0, 93, 0, 104, 0, 0, 0, 0, 0, 0, 0, 104, 0, 115, 93, 0, 0, 0, 0, 0, 0, 115, 0, 0, 104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":6, + "name":"Metadata", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, { "draworder":"topdown", "id":3, @@ -78,11 +97,11 @@ "x":0, "y":0 }], - "nextlayerid":6, + "nextlayerid":7, "nextobjectid":3, "orientation":"orthogonal", "renderorder":"right-down", - "tiledversion":"2021.03.23", + "tiledversion":"1.4.3", "tileheight":32, "tilesets":[ { @@ -100,6 +119,6 @@ }], "tilewidth":32, "type":"map", - "version":1.5, + "version":1.4, "width":10 } \ No newline at end of file From 841bf29764305e1fbdccf15eebb85aab5a9237fe Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 11:20:07 +0200 Subject: [PATCH 13/87] auto update show/hide layer --- front/src/Phaser/Game/DirtyScene.ts | 1 + front/src/Phaser/Game/GameScene.ts | 34 ++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/front/src/Phaser/Game/DirtyScene.ts b/front/src/Phaser/Game/DirtyScene.ts index 03ec9a95..e88e11f6 100644 --- a/front/src/Phaser/Game/DirtyScene.ts +++ b/front/src/Phaser/Game/DirtyScene.ts @@ -35,6 +35,7 @@ export abstract class DirtyScene extends ResizableScene { this.events.on(Events.RENDER, () => { this.objectListChanged = false; + this.dirty = false; }); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 65129787..6939721e 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -91,6 +91,7 @@ import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import {waScaleManager} from "../Services/WaScaleManager"; +import {LayerEvent} from "../../Api/Events/LayerEvent"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -839,7 +840,7 @@ ${escapedMessage} this.popUpElements.set(openPopupEvent.popupId, domElement); })); - this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { + this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { const popUpElement = this.popUpElements.get(closePopupEvent.popupId); if (popUpElement === undefined) { console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?'); @@ -857,26 +858,48 @@ ${escapedMessage} }); })); - this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(()=>{ this.userInputManager.disableControls(); })); - this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ this.userInputManager.restoreControls(); })); let scriptedBubbleSprite : Sprite; - this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); scriptedBubbleSprite.setDisplayOrigin(48, 48); this.add.existing(scriptedBubbleSprite); })); - this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(()=>{ scriptedBubbleSprite.destroy(); })); + this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent)=>{ + console.log('showLayer 3'); + this.setLayerVisibility(layerEvent.name, true); + })); + + this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{ + console.log('hideLayer 3'); + this.setLayerVisibility(layerEvent.name, false); + })); + } + private setLayerVisibility(layerName: string, visible: boolean): void { + console.log('visibility'); + const layer = this.Layers.find((layer) => layer.layer.name === layerName); + if (layer === undefined) { + console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); + return; + } + layer.setVisible(visible); + this.dirty = true; + } + + private getMapDirUrl(): string { return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); } @@ -1207,7 +1230,6 @@ ${escapedMessage} * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ update(time: number, delta: number) : void { - this.dirty = false; mediaManager.updateScene(); this.currentTick = time; if (this.CurrentPlayer.isMoving()) { From 8edd29abaab1c1d671e8cc9cca3bb465e2aec43d Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 14:43:00 +0200 Subject: [PATCH 14/87] suppression console.log --- maps/tests/iframe.html | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index 135096f8..116bbfd9 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -2,9 +2,6 @@ - @@ -22,17 +19,15 @@ }));
- +
From 973b3405ef3ff54809e110f6a6c09fc2e54ed9fe Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 15:10:11 +0200 Subject: [PATCH 15/87] documentation of show/hide layer --- docs/maps/api-reference.md | 29 +++++++++++++++++++++++++++++ maps/tests/iframe.html | 10 +++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 9891a88a..3a893474 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -235,3 +235,32 @@ mySound.play(config); // ... mySound.stop(); ``` + +### Show / Hide a layer + +``` +WA.showLayer(layerName : string): void +WA.hideLayer(layerName : string) : void +``` +These 2 methods can be used to show and hide a layer. + +Example : + +```javascript +
+ + +
+ +``` + + diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index 116bbfd9..c5c30972 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -19,15 +19,15 @@ }));
- +
From cf811c547b615ac9bbca9be19aa84e2aafe5a5f0 Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 10 May 2021 17:29:50 +0200 Subject: [PATCH 16/87] documentation of show/hide layer simplification --- docs/maps/api-reference.md | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 3a893474..d7d7f385 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -247,20 +247,9 @@ These 2 methods can be used to show and hide a layer. Example : ```javascript -
- - -
- +WA.showLayer('bottom'); +//... +WA.hideLayer('bottom'); ``` From 8e136cebe8a787433a33b13153f1e8f0d9a9f625 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 21:27:17 +0200 Subject: [PATCH 17/87] added callback on playermove - gets quite delayed after walking for a few seconds --- front/src/Api/Events/HasMovedEvent.ts | 19 ++++++ front/src/Api/Events/IframeEvent.ts | 4 +- front/src/Api/IframeListener.ts | 34 +++++++---- front/src/Phaser/Game/GameManager.ts | 7 +-- front/src/Phaser/Game/GameScene.ts | 8 ++- front/src/Phaser/Game/PlayerMovement.ts | 7 ++- .../Game/PlayersPositionInterpolator.ts | 8 +-- front/src/iframe_api.ts | 61 ++++++++++++++----- 8 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 front/src/Api/Events/HasMovedEvent.ts diff --git a/front/src/Api/Events/HasMovedEvent.ts b/front/src/Api/Events/HasMovedEvent.ts new file mode 100644 index 00000000..fef8e731 --- /dev/null +++ b/front/src/Api/Events/HasMovedEvent.ts @@ -0,0 +1,19 @@ +import * as tg from "generic-type-guard"; + + + +export const isHasMovedEvent = + new tg.IsInterface().withProperties({ + direction: tg.isString, + moving: tg.isBoolean, + x: tg.isNumber, + y: tg.isNumber + }).get(); + +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type HasMovedEvent = tg.GuardedType; + + +export type HasMovedEventCallback = (event: HasMovedEvent) => void diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index c1ad6955..f28ea85e 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -1,11 +1,11 @@ - import { GameStateEvent } from './ApiGameStateEvent'; import { ButtonClickedEvent } from './ButtonClickedEvent'; import { ChatEvent } from './ChatEvent'; import { ClosePopupEvent } from './ClosePopupEvent'; import { EnterLeaveEvent } from './EnterLeaveEvent'; import { GoToPageEvent } from './GoToPageEvent'; +import { HasMovedEvent } from './HasMovedEvent'; import { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import { OpenPopupEvent } from './OpenPopupEvent'; import { OpenTabEvent } from './OpenTabEvent'; @@ -30,6 +30,7 @@ export type IframeEventMap = { restorePlayerControl: null displayBubble: null removeBubble: null + enableMoveEvents: undefined } export interface IframeEvent { type: T; @@ -46,6 +47,7 @@ export interface IframeResponseEventMap { leaveEvent: EnterLeaveEvent buttonClickedEvent: ButtonClickedEvent gameState: GameStateEvent + hasMovedEvent: HasMovedEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index fcf4e854..f10d0fc1 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -14,6 +14,7 @@ import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMa import { UserInputChatEvent } from "./Events/UserInputChatEvent"; import { GameStateEvent } from './Events/ApiGameStateEvent'; import { deepFreezeClone as deepFreezeClone } from '../utility'; +import { HasMovedEvent } from './Events/HasMovedEvent'; /** @@ -21,6 +22,7 @@ import { deepFreezeClone as deepFreezeClone } from '../utility'; * Also allows to send messages to those iframes. */ class IframeListener { + private readonly _chatStream: Subject = new Subject(); public readonly chatStream = this._chatStream.asObservable(); @@ -54,12 +56,13 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); - + private readonly _gameStateStream: Subject = new Subject(); public readonly gameStateStream = this._gameStateStream.asObservable(); private readonly iframes = new Set(); private readonly scripts = new Map(); + private sendMoveEvents: boolean = false; init() { window.addEventListener("message", (message: TypedMessageEvent>) => { @@ -101,20 +104,18 @@ class IframeListener { } else if (payload.type === 'closeCoWebSite') { scriptUtils.closeCoWebSite(); - } - else if (payload.type === 'disablePlayerControl') { + } else if (payload.type === 'disablePlayerControl') { this._disablePlayerControlStream.next(); - } - else if (payload.type === 'restorePlayerControl') { + } else if (payload.type === 'restorePlayerControl') { this._enablePlayerControlStream.next(); - } - else if (payload.type === 'displayBubble') { + } else if (payload.type === 'displayBubble') { this._displayBubbleStream.next(); - } - else if (payload.type === 'removeBubble') { + } else if (payload.type === 'removeBubble') { this._removeBubbleStream.next(); - }else if(payload.type=="getState"){ + } else if (payload.type == "getState") { this._gameStateStream.next(); + } else if (payload.type == "enableMoveEvents") { + this.sendMoveEvents = true } } @@ -123,11 +124,11 @@ class IframeListener { } - + sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { this.postMessage({ 'type': 'gameState', - 'data': deepFreezeClone(gameStateEvent) + 'data': deepFreezeClone(gameStateEvent) }); } @@ -234,6 +235,15 @@ class IframeListener { }); } + hasMovedEvent(event: HasMovedEvent) { + if (this.sendMoveEvents) { + this.postMessage({ + 'type': 'hasMovedEvent', + 'data': event + }); + } + } + sendButtonClickedEvent(popupId: number, buttonId: number): void { this.postMessage({ 'type': 'buttonClickedEvent', diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 6047d430..157e8e80 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -8,12 +8,7 @@ import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import {EnableCameraSceneName} from "../Login/EnableCameraScene"; import {localUserStore} from "../../Connexion/LocalUserStore"; -export interface HasMovedEvent { - direction: string; - moving: boolean; - x: number; - y: number; -} + /** * This class should be responsible for any scene starting/stopping diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7d0d51d3..63efa3e6 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,4 @@ -import {gameManager, HasMovedEvent} from "./GameManager"; +import { gameManager } from "./GameManager"; import { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -91,7 +91,8 @@ import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import { PlayerStateObject } from '../../Api/Events/ApiGameStateEvent'; -import {waScaleManager} from "../Services/WaScaleManager"; +import { waScaleManager } from "../Services/WaScaleManager"; +import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -631,6 +632,9 @@ export class GameScene extends DirtyScene implements CenterListener { //listen event to share position of user this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { + iframeListener.hasMovedEvent(event) + }) this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { this.gameMap.setPosition(event.x, event.y); diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index eb1a5d1b..18c3ee0c 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -1,6 +1,7 @@ -import {HasMovedEvent} from "./GameManager"; -import {MAX_EXTRAPOLATION_TIME} from "../../Enum/EnvironmentVariable"; -import {PositionInterface} from "../../Connexion/ConnexionModels"; + +import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; +import { PositionInterface } from "../../Connexion/ConnexionModels"; +import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; export class PlayerMovement { public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) { diff --git a/front/src/Phaser/Game/PlayersPositionInterpolator.ts b/front/src/Phaser/Game/PlayersPositionInterpolator.ts index 3ac87397..321396e2 100644 --- a/front/src/Phaser/Game/PlayersPositionInterpolator.ts +++ b/front/src/Phaser/Game/PlayersPositionInterpolator.ts @@ -2,13 +2,13 @@ * This class is in charge of computing the position of all players. * Player movement is delayed by 200ms so position depends on ticks. */ -import {PlayerMovement} from "./PlayerMovement"; -import {HasMovedEvent} from "./GameManager"; +import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; +import { PlayerMovement } from "./PlayerMovement"; export class PlayersPositionInterpolator { playerMovements: Map = new Map(); - updatePlayerPosition(userId: number, playerMovement: PlayerMovement) : void { + updatePlayerPosition(userId: number, playerMovement: PlayerMovement): void { this.playerMovements.set(userId, playerMovement); } @@ -16,7 +16,7 @@ export class PlayersPositionInterpolator { this.playerMovements.delete(userId); } - getUpdatedPositions(tick: number) : Map { + getUpdatedPositions(tick: number): Map { const positions = new Map(); this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => { if (playerMovement.isOutdated(tick)) { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index cb55f1aa..9a3e63b0 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -1,5 +1,5 @@ import { ChatEvent } from "./Api/Events/ChatEvent"; -import { isIframeResponseEventWrapper } from "./Api/Events/IframeEvent"; +import { IframeEvent, IframeEventMap, isIframeResponseEventWrapper } from "./Api/Events/IframeEvent"; import { isUserInputChatEvent, UserInputChatEvent } from "./Api/Events/UserInputChatEvent"; import { Subject } from "rxjs"; import { EnterLeaveEvent, isEnterLeaveEvent } from "./Api/Events/EnterLeaveEvent"; @@ -10,6 +10,7 @@ import { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; import { GameStateEvent, isGameStateEvent } from './Api/Events/ApiGameStateEvent'; +import { HasMovedEvent, HasMovedEventCallback, isHasMovedEvent } from './Api/Events/HasMovedEvent'; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -17,15 +18,17 @@ interface WorkAdventureApi { onEnterZone(name: string, callback: () => void): void; onLeaveZone(name: string, callback: () => void): void; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup; - openTab(url : string): void; - goToPage(url : string): void; - openCoWebSite(url : string): void; + openTab(url: string): void; + goToPage(url: string): void; + openCoWebSite(url: string): void; closeCoWebSite(): void; disablePlayerControl(): void; restorePlayerControl(): void; displayBubble(): void; removeBubble(): void; - getGameState():Promise + getGameState(): Promise + + onMoveEvent(callback: (moveEvent: HasMovedEvent) => void): void } declare global { @@ -75,20 +78,44 @@ class Popup { }, '*'); } } +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +const stateResolvers: Array<(event: GameStateEvent) => void> = [] + +const callbacks: { [type: string]: HasMovedEventCallback | ((arg?: HasMovedEvent | never) => void) } = {} -const stateResolvers:Array<(event:GameStateEvent)=>void> =[] +function postToParent(content: IframeEvent) { + window.parent.postMessage(content, "*") +} +let moveEventUuid: string | undefined; window.WA = { + onMoveEvent(callback: HasMovedEventCallback): void { + moveEventUuid = uuidv4(); + callbacks[moveEventUuid] = callback; + postToParent({ + type: "enableMoveEvents", + data: undefined + }) + window.parent.postMessage({ + type: "enable" + }, "*") + }, - getGameState(){ - return new Promise((resolver,thrower)=>{ + getGameState() { + return new Promise((resolver, thrower) => { stateResolvers.push(resolver); - window.parent.postMessage({ - type:"getState" - },"*") + window.parent.postMessage({ + type: "getState" + }, "*") }) }, @@ -140,10 +167,10 @@ window.WA = { }, '*'); }, - openCoWebSite(url : string) : void{ + openCoWebSite(url: string): void { window.parent.postMessage({ - "type" : 'openCoWebSite', - "data" : { + "type": 'openCoWebSite', + "data": { url } as OpenCoWebSiteEvent }, '*'); @@ -242,10 +269,12 @@ window.addEventListener('message', message => { if (callback) { callback(popup); } - }else if(payload.type=="gameState" && isGameStateEvent(payloadData)){ - stateResolvers.forEach(resolver=>{ + } else if (payload.type == "gameState" && isGameStateEvent(payloadData)) { + stateResolvers.forEach(resolver => { resolver(payloadData); }) + } else if (payload.type == "hasMovedEvent" && isHasMovedEvent(payloadData) && moveEventUuid) { + callbacks[moveEventUuid](payloadData) } } From 2c4c98b0e56c3d064d2a93aa6464b1b8d508b4de Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 10 May 2021 21:44:15 +0200 Subject: [PATCH 18/87] limited event trigger to max 10 per second --- front/src/Api/IframeListener.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index f10d0fc1..975dde67 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -15,6 +15,8 @@ import { UserInputChatEvent } from "./Events/UserInputChatEvent"; import { GameStateEvent } from './Events/ApiGameStateEvent'; import { deepFreezeClone as deepFreezeClone } from '../utility'; import { HasMovedEvent } from './Events/HasMovedEvent'; +import { Math } from 'phaser'; + /** @@ -63,6 +65,7 @@ class IframeListener { private readonly iframes = new Set(); private readonly scripts = new Map(); private sendMoveEvents: boolean = false; + private lastMoveTimestamp: number = 0 init() { window.addEventListener("message", (message: TypedMessageEvent>) => { @@ -237,10 +240,14 @@ class IframeListener { hasMovedEvent(event: HasMovedEvent) { if (this.sendMoveEvents) { - this.postMessage({ - 'type': 'hasMovedEvent', - 'data': event - }); + if (this.lastMoveTimestamp < Date.now() - 100) { + this.lastMoveTimestamp = Date.now() + this.postMessage({ + 'type': 'hasMovedEvent', + 'data': event + }); + } + } } From 43aad4ab143242086124e4519612145666589eb4 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 12 May 2021 14:30:12 +0200 Subject: [PATCH 19/87] phaserLayers managed by Gamemap Implementation of LayersFlattener Implementation of Setting properties of a layer form script Update show/hide layer form script Update unit test of LayersIteratorTest --- front/src/Api/Events/IframeEvent.ts | 2 + front/src/Api/Events/setPropertyEvent.ts | 12 + front/src/Api/IframeListener.ts | 8 +- front/src/Phaser/Game/GameMap.ts | 39 +++- front/src/Phaser/Game/GameScene.ts | 78 ++++--- front/src/Phaser/Map/ITiledMap.ts | 3 + front/src/Phaser/Map/LayersFlattener.ts | 22 ++ front/src/Phaser/Map/LayersIterator.ts | 44 ---- front/src/iframe_api.ts | 14 +- front/tests/Phaser/Map/LayersIteratorTest.ts | 223 ++++++++++--------- maps/tests/iframe.html | 9 +- 11 files changed, 258 insertions(+), 196 deletions(-) create mode 100644 front/src/Api/Events/setPropertyEvent.ts create mode 100644 front/src/Phaser/Map/LayersFlattener.ts delete mode 100644 front/src/Phaser/Map/LayersIterator.ts diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 2e7ccd86..d0994fa5 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -10,6 +10,7 @@ import { OpenPopupEvent } from './OpenPopupEvent'; import { OpenTabEvent } from './OpenTabEvent'; import { UserInputChatEvent } from './UserInputChatEvent'; import { LayerEvent } from './LayerEvent'; +import { SetPropertyEvent } from "./setPropertyEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -32,6 +33,7 @@ export type IframeEventMap = { removeBubble: null showLayer: LayerEvent hideLayer: LayerEvent + setProperty: SetPropertyEvent } export interface IframeEvent { type: T; diff --git a/front/src/Api/Events/setPropertyEvent.ts b/front/src/Api/Events/setPropertyEvent.ts new file mode 100644 index 00000000..39785bc6 --- /dev/null +++ b/front/src/Api/Events/setPropertyEvent.ts @@ -0,0 +1,12 @@ +import * as tg from "generic-type-guard"; + +export const isSetPropertyEvent = + new tg.IsInterface().withProperties({ + layerName: tg.isString, + propertyName: tg.isString, + propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))) + }).get(); +/** + * A message sent from the iFrame to the game to change the value of the property of the layer + */ +export type SetPropertyEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 5529d36e..d8e3a8c8 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,7 +12,8 @@ import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; import { UserInputChatEvent } from "./Events/UserInputChatEvent"; -import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; +import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; +import { isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; /** @@ -59,6 +60,9 @@ class IframeListener { private readonly _hideLayerStream: Subject = new Subject(); public readonly hideLayerStream = this._hideLayerStream.asObservable(); + private readonly _setPropertyStream: Subject = new Subject(); + public readonly setPropertyStream = this._setPropertyStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -84,6 +88,8 @@ class IframeListener { this._showLayerStream.next(payload.data); } else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) { this._hideLayerStream.next(payload.data); + } else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) { + this._setPropertyStream.next(payload.data); } else if (payload.type === 'chat' && isChatEvent(payload.data)) { this._chatStream.next(payload.data); } else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) { diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 5fe91b62..b8b68e15 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,5 +1,5 @@ -import {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap"; -import {LayersIterator} from "../Map/LayersIterator"; +import {ITiledMap, ITiledMapLayer, ITiledMapTileLayer} from "../Map/ITiledMap"; +import { flattenGroupLayersMap } from "../Map/LayersFlattener"; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -11,10 +11,19 @@ export class GameMap { private key: number|undefined; private lastProperties = new Map(); private callbacks = new Map>(); - public readonly layersIterator: LayersIterator; + public readonly flatLayers: ITiledMapLayer[]; - public constructor(private map: ITiledMap) { - this.layersIterator = new LayersIterator(map); + public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array) { + this.flatLayers = flattenGroupLayersMap(map); + let depth = -2; + for (const layer of this.flatLayers) { + if(layer.type === 'tilelayer'){ + layer.phaserLayer = phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth); + } + if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { + depth = 10000; + } + } } /** @@ -58,7 +67,7 @@ export class GameMap { private getProperties(key: number): Map { const properties = new Map(); - for (const layer of this.layersIterator) { + for (const layer of this.flatLayers) { if (layer.type !== 'tilelayer') { continue; } @@ -100,4 +109,22 @@ export class GameMap { } callbacksArray.push(callback); } + + public findLayer(layerName: string): ITiledMapLayer | undefined { + let i = 0; + let found = false; + while (!found && i = new Map(); Map!: Phaser.Tilemaps.Tilemap; - Layers!: Array; Objects!: Array; mapFile!: ITiledMap; groups: Map; @@ -392,7 +392,6 @@ export class GameScene extends DirtyScene implements CenterListener { //initalise map 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) => { this.Terrains.push(this.Map.addTilesetImage(tileset.name, `${mapDirUrl}/${tileset.image}`, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing/*, tileset.firstgid*/)); @@ -402,11 +401,9 @@ export class GameScene extends DirtyScene implements CenterListener { this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); //add layer on map - this.Layers = new Array(); - let depth = -2; - for (const layer of this.gameMap.layersIterator) { + this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); + for (const layer of this.gameMap.flatLayers) { if (layer.type === 'tilelayer') { - this.addLayer(this.Map.createLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { @@ -417,9 +414,6 @@ export class GameScene extends DirtyScene implements CenterListener { this.loadNextGame(exitUrl); } } - if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { - depth = 10000; - } if (layer.type === 'objectgroup') { for (const object of layer.objects) { if (object.text) { @@ -428,9 +422,6 @@ export class GameScene extends DirtyScene implements CenterListener { } } } - if (depth === -2) { - throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at. This layer cannot be contained in a group.'); - } this.initStartXAndStartY(); @@ -884,15 +875,38 @@ ${escapedMessage} this.setLayerVisibility(layerEvent.name, false); })); + this.iframeSubscriptionList.push(iframeListener.setPropertyStream.subscribe((setProperty) => { + this.setPropertyLayer(setProperty.layerName, setProperty.propertyName, setProperty.propertyValue); + })); + + } + + private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { + const layer = this.gameMap.findLayer(layerName); + if (layer === undefined) { + console.warn('Could not find layer "' + layerName + '" when calling setProperty'); + return; + } + const property = (layer.properties as ITiledMapLayerProperty[])?.find((property) => property.name === propertyName); + if (property === undefined) { + layer.properties = []; + layer.properties.push({name : propertyName, type : typeof propertyValue, value : propertyValue}); + return; + } + property.value = propertyValue; } private setLayerVisibility(layerName: string, visible: boolean): void { - const layer = this.Layers.find((layer) => layer.layer.name === layerName); + const layer = this.gameMap.findLayer(layerName); if (layer === undefined) { console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); return; } - layer.setVisible(visible); + if(layer.type != "tilelayer"){ + console.warn('The layer "' + layerName + '" is not a tilelayer. It can not be show/hide'); + return; + } + layer.phaserLayer?.setVisible(visible); this.dirty = true; } @@ -1001,7 +1015,7 @@ ${escapedMessage} } private initPositionFromLayerName(layerName: string) { - for (const layer of this.gameMap.layersIterator) { + for (const layer of this.gameMap.flatLayers) { if ((layerName === layer.name || layer.name.endsWith('/'+layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { const startPosition = this.startUser(layer); this.startX = startPosition.x + this.mapFile.tilewidth/2; @@ -1091,27 +1105,29 @@ ${escapedMessage} this.updateCameraOffset(); } - addLayer(Layer : Phaser.Tilemaps.TilemapLayer){ - this.Layers.push(Layer); - } - createCollisionWithPlayer() { this.physics.disableUpdate(); //add collision layer - this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { - this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { - //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) - }); - Layer.setCollisionByProperty({collides: true}); - if (DEBUG_MODE) { - //debug code to see the collision hitbox of the object in the top layer - Layer.renderDebug(this.add.graphics(), { - tileColor: null, //non-colliding tiles - collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200), // Colliding tiles, - faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges + for (const Layer of this.gameMap.flatLayers) { + if (Layer.type == "tilelayer") { + if (Layer.phaserLayer === undefined) { + throw new Error('phaserLayer of layer "' + Layer.name + '" is undefined'); + } + this.physics.add.collider(this.CurrentPlayer, Layer.phaserLayer, (object1: GameObject, object2: GameObject) => { + //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); + Layer.phaserLayer.setCollisionByProperty({collides: true}); + if (DEBUG_MODE) { + //debug code to see the collision hitbox of the object in the top layer + Layer.phaserLayer.renderDebug(this.add.graphics(), { + tileColor: null, //non-colliding tiles + collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200), // Colliding tiles, + faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges + }); + } + //}); } - }); + } } createCurrentPlayer(){ diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index c4828911..d381e9d4 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -4,6 +4,8 @@ * Represents the interface for the Tiled exported data structure (JSON). Used * when loading resources via Resource loader. */ +import TilemapLayer = Phaser.Tilemaps.TilemapLayer; + export interface ITiledMap { width: number; height: number; @@ -81,6 +83,7 @@ export interface ITiledMapTileLayer { * Draw order (topdown (default), index) */ draworder?: string; + phaserLayer?: TilemapLayer; } export interface ITiledMapObjectLayer { diff --git a/front/src/Phaser/Map/LayersFlattener.ts b/front/src/Phaser/Map/LayersFlattener.ts new file mode 100644 index 00000000..a3b12522 --- /dev/null +++ b/front/src/Phaser/Map/LayersFlattener.ts @@ -0,0 +1,22 @@ +import {ITiledMap, ITiledMapLayer} from "./ITiledMap"; + +/** + * Flatten the grouped layers + */ +export function flattenGroupLayersMap(map: ITiledMap) { + let flatLayers: ITiledMapLayer[] = []; + flattenGroupLayers(map.layers, '', flatLayers); + return flatLayers; +} + +function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLayers: ITiledMapLayer[]) { + for (const layer of layers) { + if (layer.type === 'group') { + flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers); + } else { + const layerWithNewName = { ...layer }; + layerWithNewName.name = prefix+layerWithNewName.name; + flatLayers.push(layerWithNewName); + } + } +} \ No newline at end of file diff --git a/front/src/Phaser/Map/LayersIterator.ts b/front/src/Phaser/Map/LayersIterator.ts deleted file mode 100644 index 501a5f7b..00000000 --- a/front/src/Phaser/Map/LayersIterator.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {ITiledMap, ITiledMapLayer} from "./ITiledMap"; - -/** - * Iterates over the layers of a map, flattening the grouped layers - */ -export class LayersIterator implements IterableIterator { - - private layers: ITiledMapLayer[] = []; - private pointer: number = 0; - - constructor(private map: ITiledMap) { - this.initLayersList(map.layers, ''); - } - - private initLayersList(layers : ITiledMapLayer[], prefix : string) { - for (const layer of layers) { - if (layer.type === 'group') { - this.initLayersList(layer.layers, prefix + layer.name + '/'); - } else { - const layerWithNewName = { ...layer }; - layerWithNewName.name = prefix+layerWithNewName.name; - this.layers.push(layerWithNewName); - } - } - } - - public next(): IteratorResult { - if (this.pointer < this.layers.length) { - return { - done: false, - value: this.layers[this.pointer++] - } - } else { - return { - done: true, - value: null - } - } - } - - [Symbol.iterator](): IterableIterator { - return new LayersIterator(this.map); - } -} diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 9f059cd0..a96ad193 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,7 +9,8 @@ import { ClosePopupEvent } from "./Api/Events/ClosePopupEvent"; import { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; -import {LayerEvent} from "./Api/Events/LayerEvent"; +import { LayerEvent } from "./Api/Events/LayerEvent"; +import { SetPropertyEvent } from "./Api/Events/setPropertyEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -27,6 +28,7 @@ interface WorkAdventureApi { removeBubble() : void; showLayer(layer: string) : void; hideLayer(layer: string) : void; + setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void; } declare global { @@ -107,6 +109,16 @@ window.WA = { } as LayerEvent }, '*'); }, + setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { + window.parent.postMessage({ + 'type' : 'setProperty', + 'data' : { + 'layerName' : layerName, + 'propertyName' : propertyName, + 'propertyValue' : propertyValue + } as SetPropertyEvent + }, '*'); + }, disablePlayerControls(): void { window.parent.postMessage({ 'type': 'disablePlayerControls' }, '*'); }, diff --git a/front/tests/Phaser/Map/LayersIteratorTest.ts b/front/tests/Phaser/Map/LayersIteratorTest.ts index 3b9d0d9b..de95ecef 100644 --- a/front/tests/Phaser/Map/LayersIteratorTest.ts +++ b/front/tests/Phaser/Map/LayersIteratorTest.ts @@ -1,145 +1,148 @@ import "jasmine"; import {Room} from "../../../src/Connexion/Room"; -import {LayersIterator} from "../../../src/Phaser/Map/LayersIterator"; +import {flattenGroupLayersMap} from "../../../src/Phaser/Map/LayersFlattener"; +import {ITiledMapLayer} from "../../../src/Phaser/Map/ITiledMap"; -describe("Layers iterator", () => { +describe("Layers flattener", () => { it("should iterate maps with no group", () => { - const layersIterator = new LayersIterator({ - "compressionlevel":-1, - "height":2, - "infinite":false, - "layers":[ + let flatLayers:ITiledMapLayer[] = []; + flatLayers = flattenGroupLayersMap({ + "compressionlevel": -1, + "height": 2, + "infinite": false, + "layers": [ { - "data":[0, 0, 0, 0], - "height":2, - "id":1, - "name":"Tile Layer 1", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 1, + "name": "Tile Layer 1", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }, { - "data":[0, 0, 0, 0], - "height":2, - "id":1, - "name":"Tile Layer 2", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 1, + "name": "Tile Layer 2", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }], - "nextlayerid":2, - "nextobjectid":1, - "orientation":"orthogonal", - "renderorder":"right-down", - "tiledversion":"2021.03.23", - "tileheight":32, - "tilesets":[], - "tilewidth":32, - "type":"map", - "version":1.5, - "width":2 + "nextlayerid": 2, + "nextobjectid": 1, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "2021.03.23", + "tileheight": 32, + "tilesets": [], + "tilewidth": 32, + "type": "map", + "version": 1.5, + "width": 2 }) const layers = []; - for (const layer of layersIterator) { + for (const layer of flatLayers) { layers.push(layer.name); } expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']); }); it("should iterate maps with recursive groups", () => { - const layersIterator = new LayersIterator({ - "compressionlevel":-1, - "height":2, - "infinite":false, - "layers":[ + let flatLayers:ITiledMapLayer[] = []; + flatLayers = flattenGroupLayersMap({ + "compressionlevel": -1, + "height": 2, + "infinite": false, + "layers": [ { - "id":6, - "layers":[ + "id": 6, + "layers": [ { - "id":5, - "layers":[ + "id": 5, + "layers": [ { - "data":[0, 0, 0, 0], - "height":2, - "id":10, - "name":"Tile3", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 10, + "name": "Tile3", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }, { - "data":[0, 0, 0, 0], - "height":2, - "id":9, - "name":"Tile2", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 9, + "name": "Tile2", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }], - "name":"Group 3", - "opacity":1, - "type":"group", - "visible":true, - "x":0, - "y":0 + "name": "Group 3", + "opacity": 1, + "type": "group", + "visible": true, + "x": 0, + "y": 0 }, { - "id":7, - "layers":[ + "id": 7, + "layers": [ { - "data":[0, 0, 0, 0], - "height":2, - "id":8, - "name":"Tile1", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":2, - "x":0, - "y":0 + "data": [0, 0, 0, 0], + "height": 2, + "id": 8, + "name": "Tile1", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 2, + "x": 0, + "y": 0 }], - "name":"Group 2", - "opacity":1, - "type":"group", - "visible":true, - "x":0, - "y":0 + "name": "Group 2", + "opacity": 1, + "type": "group", + "visible": true, + "x": 0, + "y": 0 }], - "name":"Group 1", - "opacity":1, - "type":"group", - "visible":true, - "x":0, - "y":0 + "name": "Group 1", + "opacity": 1, + "type": "group", + "visible": true, + "x": 0, + "y": 0 }], - "nextlayerid":11, - "nextobjectid":1, - "orientation":"orthogonal", - "renderorder":"right-down", - "tiledversion":"2021.03.23", - "tileheight":32, - "tilesets":[], - "tilewidth":32, - "type":"map", - "version":1.5, - "width":2 + "nextlayerid": 11, + "nextobjectid": 1, + "orientation": "orthogonal", + "renderorder": "right-down", + "tiledversion": "2021.03.23", + "tileheight": 32, + "tilesets": [], + "tilewidth": 32, + "type": "map", + "version": 1.5, + "width": 2 }) const layers = []; - for (const layer of layersIterator) { + for (const layer of flatLayers) { layers.push(layer.name); } expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']); diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index c5c30972..f9f43f20 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -19,17 +19,20 @@ }));
- +
+ From 39539214df3ae7993bc13af738898ae95fafe2fe Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 17 May 2021 10:13:48 +0200 Subject: [PATCH 20/87] documentation of SetProperty --- docs/maps/api-reference.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index d7d7f385..6e98dfb5 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -252,4 +252,15 @@ WA.showLayer('bottom'); WA.hideLayer('bottom'); ``` +### Set/Create properties in a layer + +``` +WA.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; +``` + +Set the value of the "propertyName" property of the layer "layerName" at "propertyValue". If the property doesn't exist, create the property "propertyName" and set the value of the property at "propertyValue". + +```javascript +WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); +``` From 9b68faac0e491b58a8a0b30734d740f0b916b34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 18 May 2021 09:53:54 +0200 Subject: [PATCH 21/87] Fixing JSDoc --- front/src/Api/Events/MenuItemClickedEvent.ts | 2 +- front/src/Api/Events/MenuItemRegisterEvent.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/Api/Events/MenuItemClickedEvent.ts b/front/src/Api/Events/MenuItemClickedEvent.ts index dd80c0f2..0735eda4 100644 --- a/front/src/Api/Events/MenuItemClickedEvent.ts +++ b/front/src/Api/Events/MenuItemClickedEvent.ts @@ -5,6 +5,6 @@ export const isMenuItemClickedEvent = menuItem: tg.isString }).get(); /** - * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. + * A message sent from the game to the iFrame when a menu item is clicked. */ export type MenuItemClickedEvent = tg.GuardedType; diff --git a/front/src/Api/Events/MenuItemRegisterEvent.ts b/front/src/Api/Events/MenuItemRegisterEvent.ts index 98d4c7d3..a25e5cc3 100644 --- a/front/src/Api/Events/MenuItemRegisterEvent.ts +++ b/front/src/Api/Events/MenuItemRegisterEvent.ts @@ -5,6 +5,6 @@ export const isMenuItemRegisterEvent = menutItem: tg.isString }).get(); /** - * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. + * A message sent from the iFrame to the game to add a new menu item. */ export type MenuItemRegisterEvent = tg.GuardedType; From 3edfd5b285c5d2f51eab85c0c1fc865a04ffcc8e Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 18 May 2021 11:33:16 +0200 Subject: [PATCH 22/87] GameState is now save in cache HasPlayerMoved is send when the player is actually moving on the map every 200ms. --- ...ApiGameStateEvent.ts => GameStateEvent.ts} | 9 +- .../Api/Events/HasDataLayerChangedEvent.ts | 16 ++ front/src/Api/Events/HasMovedEvent.ts | 19 -- front/src/Api/Events/HasPlayerMovedEvent.ts | 19 ++ front/src/Api/Events/IframeEvent.ts | 11 +- front/src/Api/IframeListener.ts | 42 ++-- front/src/Phaser/Game/GameScene.ts | 28 +-- front/src/Phaser/Game/PlayerMovement.ts | 6 +- .../Game/PlayersPositionInterpolator.ts | 6 +- front/src/iframe_api.ts | 109 ++++++--- maps/tests/Metadata/map.json | 230 ++++++++++++++++++ maps/tests/Metadata/script.js | 9 + maps/tests/Metadata/tileset_dungeon.png | Bin 0 -> 9696 bytes 13 files changed, 404 insertions(+), 100 deletions(-) rename front/src/Api/Events/{ApiGameStateEvent.ts => GameStateEvent.ts} (72%) create mode 100644 front/src/Api/Events/HasDataLayerChangedEvent.ts delete mode 100644 front/src/Api/Events/HasMovedEvent.ts create mode 100644 front/src/Api/Events/HasPlayerMovedEvent.ts create mode 100644 maps/tests/Metadata/map.json create mode 100644 maps/tests/Metadata/script.js create mode 100644 maps/tests/Metadata/tileset_dungeon.png diff --git a/front/src/Api/Events/ApiGameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts similarity index 72% rename from front/src/Api/Events/ApiGameStateEvent.ts rename to front/src/Api/Events/GameStateEvent.ts index 4f4e98ff..418d1ca0 100644 --- a/front/src/Api/Events/ApiGameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -1,6 +1,6 @@ import * as tg from "generic-type-guard"; -export const isPositionState = new tg.IsInterface().withProperties({ +/*export const isPositionState = new tg.IsInterface().withProperties({ x: tg.isNumber, y: tg.isNumber }).get() @@ -12,19 +12,16 @@ export const isPlayerState = new tg.IsInterface() }).get() ).get() -export type PlayerStateObject = tg.GuardedType; +export type PlayerStateObject = tg.GuardedType;*/ export const isGameStateEvent = new tg.IsInterface().withProperties({ roomId: tg.isString, - data: tg.isObject, mapUrl: tg.isString, - nickName: tg.isString, uuid: tg.isUnion(tg.isString, tg.isUndefined), - players: isPlayerState, startLayerName: tg.isUnion(tg.isString, tg.isNull) }).get(); /** - * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. + * A message sent from the game to the iFrame when the gameState is got by the script */ export type GameStateEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/Events/HasDataLayerChangedEvent.ts b/front/src/Api/Events/HasDataLayerChangedEvent.ts new file mode 100644 index 00000000..7714f978 --- /dev/null +++ b/front/src/Api/Events/HasDataLayerChangedEvent.ts @@ -0,0 +1,16 @@ +import * as tg from "generic-type-guard"; + + + +export const isHasDataLayerChangedEvent = + new tg.IsInterface().withProperties({ + data: tg.isObject + }).get(); + +/** + * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers + */ +export type HasDataLayerChangedEvent = tg.GuardedType; + + +export type HasDataLayerChangedEventCallback = (event: HasDataLayerChangedEvent) => void \ No newline at end of file diff --git a/front/src/Api/Events/HasMovedEvent.ts b/front/src/Api/Events/HasMovedEvent.ts deleted file mode 100644 index fef8e731..00000000 --- a/front/src/Api/Events/HasMovedEvent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as tg from "generic-type-guard"; - - - -export const isHasMovedEvent = - new tg.IsInterface().withProperties({ - direction: tg.isString, - moving: tg.isBoolean, - x: tg.isNumber, - y: tg.isNumber - }).get(); - -/** - * A message sent from the iFrame to the game to add a message in the chat. - */ -export type HasMovedEvent = tg.GuardedType; - - -export type HasMovedEventCallback = (event: HasMovedEvent) => void diff --git a/front/src/Api/Events/HasPlayerMovedEvent.ts b/front/src/Api/Events/HasPlayerMovedEvent.ts new file mode 100644 index 00000000..28603284 --- /dev/null +++ b/front/src/Api/Events/HasPlayerMovedEvent.ts @@ -0,0 +1,19 @@ +import * as tg from "generic-type-guard"; + + + +export const isHasPlayerMovedEvent = + new tg.IsInterface().withProperties({ + direction: tg.isString, + moving: tg.isBoolean, + x: tg.isNumber, + y: tg.isNumber + }).get(); + +/** + * A message sent from the game to the iFrame when the player move after the iFrame send a message to the game that it want to listen to the position of the player + */ +export type HasPlayerMovedEvent = tg.GuardedType; + + +export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 307b09fc..ae0eab34 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -1,15 +1,16 @@ -import { GameStateEvent } from './ApiGameStateEvent'; +import { GameStateEvent } from './GameStateEvent'; import { ButtonClickedEvent } from './ButtonClickedEvent'; import { ChatEvent } from './ChatEvent'; import { ClosePopupEvent } from './ClosePopupEvent'; import { EnterLeaveEvent } from './EnterLeaveEvent'; import { GoToPageEvent } from './GoToPageEvent'; -import { HasMovedEvent } from './HasMovedEvent'; +import { HasPlayerMovedEvent } from './HasPlayerMovedEvent'; import { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import { OpenPopupEvent } from './OpenPopupEvent'; import { OpenTabEvent } from './OpenTabEvent'; import { UserInputChatEvent } from './UserInputChatEvent'; +import { HasDataLayerChangedEvent } from "./HasDataLayerChangedEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -30,7 +31,8 @@ export type IframeEventMap = { restorePlayerControls: null displayBubble: null removeBubble: null - enableMoveEvents: undefined + onPlayerMove: undefined + onDataLayerChange: undefined } export interface IframeEvent { type: T; @@ -47,7 +49,8 @@ export interface IframeResponseEventMap { leaveEvent: EnterLeaveEvent buttonClickedEvent: ButtonClickedEvent gameState: GameStateEvent - hasMovedEvent: HasMovedEvent + hasPlayerMoved: HasPlayerMovedEvent + hasDataLayerChanged: HasDataLayerChangedEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 82dd23cf..d6c02516 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -12,10 +12,11 @@ import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; import { UserInputChatEvent } from "./Events/UserInputChatEvent"; -import { GameStateEvent } from './Events/ApiGameStateEvent'; +import { GameStateEvent } from './Events/GameStateEvent'; import { deepFreezeClone as deepFreezeClone } from '../utility'; -import { HasMovedEvent } from './Events/HasMovedEvent'; +import { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent'; import { Math } from 'phaser'; +import { HasDataLayerChangedEvent } from "./Events/HasDataLayerChangedEvent"; @@ -58,14 +59,14 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); - private readonly _gameStateStream: Subject = new Subject(); public readonly gameStateStream = this._gameStateStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); - private sendMoveEvents: boolean = false; - private lastMoveTimestamp: number = 0 + private sendPlayerMove: boolean = false; + private sendDataLayerChange: boolean = false; init() { window.addEventListener("message", (message: TypedMessageEvent>) => { @@ -119,8 +120,10 @@ class IframeListener { this._removeBubbleStream.next(); } else if (payload.type == "getState") { this._gameStateStream.next(); - } else if (payload.type == "enableMoveEvents") { - this.sendMoveEvents = true + } else if (payload.type == "onPlayerMove") { + this.sendPlayerMove = true + } else if (payload.type == "onDataLayerChange") { + this.sendDataLayerChange = true } } @@ -133,7 +136,7 @@ class IframeListener { sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { this.postMessage({ 'type': 'gameState', - 'data': deepFreezeClone(gameStateEvent) + 'data': gameStateEvent //deepFreezeClone(gameStateEvent) }); } @@ -240,16 +243,21 @@ class IframeListener { }); } - hasMovedEvent(event: HasMovedEvent) { - if (this.sendMoveEvents) { - if (this.lastMoveTimestamp < Date.now() - 100) { - this.lastMoveTimestamp = Date.now() - this.postMessage({ - 'type': 'hasMovedEvent', - 'data': event - }); - } + hasPlayerMoved(event: HasPlayerMovedEvent) { + if (this.sendPlayerMove) { + this.postMessage({ + 'type': 'hasPlayerMoved', + 'data': event + }); + } + } + hasDataLayerChanged(event: HasDataLayerChangedEvent) { + if (this.sendDataLayerChange) { + this.postMessage({ + 'type' : 'hasDataLayerChanged', + 'data' : event + }); } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 39fa79db..83256cec 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -90,9 +90,9 @@ import { TextUtils } from "../Components/TextUtils"; import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { PinchManager } from "../UserInput/PinchManager"; import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; -import { PlayerStateObject } from '../../Api/Events/ApiGameStateEvent'; +//import { PlayerStateObject } from '../../Api/Events/GameStateEvent'; import { waScaleManager } from "../Services/WaScaleManager"; -import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; +import { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; export interface GameSceneInitInterface { initPosition: PointInterface | null, @@ -164,7 +164,7 @@ export class GameScene extends DirtyScene implements CenterListener { currentTick!: number; lastSentTick!: number; // The last tick at which a position was sent. - lastMoveEventSent: HasMovedEvent = { + lastMoveEventSent: HasPlayerMovedEvent = { direction: '', moving: false, x: -1000, @@ -632,11 +632,11 @@ export class GameScene extends DirtyScene implements CenterListener { //listen event to share position of user this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { - iframeListener.hasMovedEvent(event) + this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => { + //iframeListener.hasMovedEvent(event) }) this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { + this.CurrentPlayer.on(hasMovedEventName, (event: HasPlayerMovedEvent) => { this.gameMap.setPosition(event.x, event.y); }) @@ -870,7 +870,7 @@ ${escapedMessage} this.userInputManager.restoreControls(); })); this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { - const playerObject: PlayerStateObject = { + /*const playerObject: PlayerStateObject = { [this.playerName]: { position: { x: this.CurrentPlayer.x, @@ -889,15 +889,12 @@ ${escapedMessage} pusherId: remotePlayer.userId } - } + }*/ iframeListener.sendFrozenGameStateEvent({ mapUrl: this.MapUrlFile, - nickName: this.playerName, startLayerName: this.startLayerName, uuid: localUserStore.getLocalUser()?.uuid, roomId: this.RoomId, - data: this.mapFile, - players: playerObject }) })); @@ -1158,7 +1155,7 @@ ${escapedMessage} this.createCollisionWithPlayer(); } - pushPlayerPosition(event: HasMovedEvent) { + pushPlayerPosition(event: HasPlayerMovedEvent) { if (this.lastMoveEventSent === event) { return; } @@ -1188,7 +1185,7 @@ ${escapedMessage} * Finds the correct item to outline and outline it (if there is an item to be outlined) * @param event */ - private outlineItem(event: HasMovedEvent): void { + private outlineItem(event: HasPlayerMovedEvent): void { let x = event.x; let y = event.y; switch (event.direction) { @@ -1227,7 +1224,7 @@ ${escapedMessage} this.outlinedItem?.selectable(); } - private doPushPlayerPosition(event: HasMovedEvent): void { + private doPushPlayerPosition(event: HasPlayerMovedEvent): void { this.lastMoveEventSent = event; this.lastSentTick = this.currentTick; const camera = this.cameras.main; @@ -1237,6 +1234,7 @@ ${escapedMessage} right: camera.scrollX + camera.width, bottom: camera.scrollY + camera.height, }); + iframeListener.hasPlayerMoved(event); } /** @@ -1286,7 +1284,7 @@ ${escapedMessage} } // Let's move all users const updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); - updatedPlayersPositions.forEach((moveEvent: HasMovedEvent, userId: number) => { + updatedPlayersPositions.forEach((moveEvent: HasPlayerMovedEvent, userId: number) => { this.dirty = true; const player: RemotePlayer | undefined = this.MapPlayersByKey.get(userId); if (player === undefined) { diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index 18c3ee0c..5680d7de 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -1,10 +1,10 @@ import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; import { PositionInterface } from "../../Connexion/ConnexionModels"; -import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; +import { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; export class PlayerMovement { - public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) { + public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasPlayerMovedEvent, private endTick: number) { } public isOutdated(tick: number): boolean { @@ -18,7 +18,7 @@ export class PlayerMovement { return tick > this.endTick + MAX_EXTRAPOLATION_TIME; } - public getPosition(tick: number): HasMovedEvent { + public getPosition(tick: number): HasPlayerMovedEvent { // Special case: end position reached and end position is not moving if (tick >= this.endTick && this.endPosition.moving === false) { //console.log('Movement finished ', this.endPosition) diff --git a/front/src/Phaser/Game/PlayersPositionInterpolator.ts b/front/src/Phaser/Game/PlayersPositionInterpolator.ts index 321396e2..53578884 100644 --- a/front/src/Phaser/Game/PlayersPositionInterpolator.ts +++ b/front/src/Phaser/Game/PlayersPositionInterpolator.ts @@ -2,7 +2,7 @@ * This class is in charge of computing the position of all players. * Player movement is delayed by 200ms so position depends on ticks. */ -import { HasMovedEvent } from '../../Api/Events/HasMovedEvent'; +import { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import { PlayerMovement } from "./PlayerMovement"; export class PlayersPositionInterpolator { @@ -16,8 +16,8 @@ export class PlayersPositionInterpolator { this.playerMovements.delete(userId); } - getUpdatedPositions(tick: number): Map { - const positions = new Map(); + getUpdatedPositions(tick: number): Map { + const positions = new Map(); this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => { if (playerMovement.isOutdated(tick)) { //console.log("outdated") diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 17c489ca..c2e91ea5 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,8 +9,9 @@ import { ClosePopupEvent } from "./Api/Events/ClosePopupEvent"; import { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; -import { GameStateEvent, isGameStateEvent } from './Api/Events/ApiGameStateEvent'; -import { HasMovedEvent, HasMovedEventCallback, isHasMovedEvent } from './Api/Events/HasMovedEvent'; +import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent'; +import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent'; +import { HasDataLayerChangedEvent, HasDataLayerChangedEventCallback, isHasDataLayerChangedEvent} from "./Api/Events/HasDataLayerChangedEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -26,9 +27,14 @@ interface WorkAdventureApi { restorePlayerControls(): void; displayBubble(): void; removeBubble(): void; - getGameState(): Promise + getMapUrl(): Promise; + getUuid(): Promise; + getRoomId(): Promise; + getStartLayerName(): Promise; - onMoveEvent(callback: (moveEvent: HasMovedEvent) => void): void + + onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void + onDataLayerChange(callback: (dataLayerChangedEvent: HasDataLayerChangedEvent) => void): void } declare global { @@ -84,41 +90,75 @@ function uuidv4() { return v.toString(16); }); } - -const stateResolvers: Array<(event: GameStateEvent) => void> = [] - -const callbacks: { [type: string]: HasMovedEventCallback | ((arg?: HasMovedEvent | never) => void) } = {} - - -function postToParent(content: IframeEvent) { - window.parent.postMessage(content, "*") -} -let moveEventUuid: string | undefined; - -window.WA = { - - onMoveEvent(callback: HasMovedEventCallback): void { - moveEventUuid = uuidv4(); - callbacks[moveEventUuid] = callback; - postToParent({ - type: "enableMoveEvents", - data: undefined - }) - - window.parent.postMessage({ - type: "enable" - }, "*") - }, - - getGameState() { +function getGameState(): Promise { + if (immutableData) { + return Promise.resolve(immutableData); + } + else { return new Promise((resolver, thrower) => { stateResolvers.push(resolver); window.parent.postMessage({ type: "getState" }, "*") }) + } +} + +const stateResolvers: Array<(event: GameStateEvent) => void> = [] +let immutableData: GameStateEvent; + +const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} +const callbackDataLayerChanged: { [type: string]: HasDataLayerChangedEventCallback | ((arg?: HasDataLayerChangedEvent | never) => void) } = {} + + +function postToParent(content: IframeEvent) { + window.parent.postMessage(content, "*") +} +let playerUuid: string | undefined; + +window.WA = { + + onPlayerMove(callback: HasPlayerMovedEventCallback): void { + playerUuid = uuidv4(); + callbackPlayerMoved[playerUuid] = callback; + postToParent({ + type: "onPlayerMove", + data: undefined + }) }, + onDataLayerChange(callback: HasDataLayerChangedEventCallback): void { + callbackDataLayerChanged['test'] = callback; + postToParent({ + type : "onDataLayerChange", + data: undefined + }) + }, + + + getMapUrl() { + return getGameState().then((res) => { + return res.mapUrl; + }) + }, + + getUuid() { + return getGameState().then((res) => { + return res.uuid; + }) + }, + + getRoomId() { + return getGameState().then((res) => { + return res.roomId; + }) + }, + + getStartLayerName() { + return getGameState().then((res) => { + return res.startLayerName; + }) + }, /** * Send a message in the chat. @@ -273,8 +313,11 @@ window.addEventListener('message', message => { stateResolvers.forEach(resolver => { resolver(payloadData); }) - } else if (payload.type == "hasMovedEvent" && isHasMovedEvent(payloadData) && moveEventUuid) { - callbacks[moveEventUuid](payloadData) + immutableData = payloadData; + } else if (payload.type == "hasPlayerMoved" && isHasPlayerMovedEvent(payloadData) && playerUuid) { + callbackPlayerMoved[playerUuid](payloadData) + } else if (payload.type == "hasDataLayerChanged" && isHasDataLayerChangedEvent(payloadData)) { + callbackDataLayerChanged['test'](payloadData) } } diff --git a/maps/tests/Metadata/map.json b/maps/tests/Metadata/map.json new file mode 100644 index 00000000..8967ed02 --- /dev/null +++ b/maps/tests/Metadata/map.json @@ -0,0 +1,230 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 33, 34, 34, 34, 34, 34, 34, 35, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 49, 50, 50, 50, 50, 50, 50, 51, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "data":[1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19], + "height":10, + "id":3, + "name":"wall", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }], + "nextlayerid":6, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js new file mode 100644 index 00000000..f3ac255a --- /dev/null +++ b/maps/tests/Metadata/script.js @@ -0,0 +1,9 @@ + + +WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); +WA.getUuid().then((uuid) => {console.log('Uuid : ',uuid)}); +WA.getRoomId().then((roomId) => console.log('roomID : ',roomId)); + +WA.listenPositionPlayer(console.log); + + diff --git a/maps/tests/Metadata/tileset_dungeon.png b/maps/tests/Metadata/tileset_dungeon.png new file mode 100644 index 0000000000000000000000000000000000000000..fcac082c33704b31451bc8a58af5982c06586aa6 GIT binary patch literal 9696 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 Date: Tue, 18 May 2021 15:41:16 +0200 Subject: [PATCH 23/87] implementation of DataLayerEvent update GetGameState to add nickname to the returned data update GameMap to separate phaserLayer and mapLayer --- front/src/Api/Events/DataLayerEvent.ts | 7 ++--- front/src/Api/Events/GameStateEvent.ts | 1 + front/src/Api/Events/IframeEvent.ts | 5 ++-- front/src/Api/IframeListener.ts | 34 +++++++++++------------ front/src/Phaser/Game/GameMap.ts | 26 +++++++++++++++++- front/src/Phaser/Game/GameScene.ts | 36 ++++++++++++------------- front/src/Phaser/Game/PlayerMovement.ts | 1 - front/src/Phaser/Map/LayersFlattener.ts | 5 ++-- front/src/iframe_api.ts | 36 ++++++++++++++++--------- maps/tests/Metadata/script.js | 10 +++---- 10 files changed, 94 insertions(+), 67 deletions(-) diff --git a/front/src/Api/Events/DataLayerEvent.ts b/front/src/Api/Events/DataLayerEvent.ts index 8d2ffa23..096d6ef5 100644 --- a/front/src/Api/Events/DataLayerEvent.ts +++ b/front/src/Api/Events/DataLayerEvent.ts @@ -2,7 +2,7 @@ import * as tg from "generic-type-guard"; -export const isHasDataLayerChangedEvent = +export const isDataLayerEvent = new tg.IsInterface().withProperties({ data: tg.isObject }).get(); @@ -10,7 +10,4 @@ export const isHasDataLayerChangedEvent = /** * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers */ -export type DataLayerEvent = tg.GuardedType; - - -export type HasDataLayerChangedEventCallback = (event: DataLayerEvent) => void \ No newline at end of file +export type DataLayerEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 418d1ca0..72e40898 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -18,6 +18,7 @@ export const isGameStateEvent = new tg.IsInterface().withProperties({ roomId: tg.isString, mapUrl: tg.isString, + nickname: tg.isUnion(tg.isString, tg.isNull), uuid: tg.isUnion(tg.isString, tg.isUndefined), startLayerName: tg.isUnion(tg.isString, tg.isNull) }).get(); diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 3ba5529f..e267fe90 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -10,7 +10,7 @@ import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenTabEvent } from './OpenTabEvent'; import type { UserInputChatEvent } from './UserInputChatEvent'; -import type { HasDataLayerChangedEvent } from "./HasDataLayerChangedEvent"; +import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; @@ -37,6 +37,7 @@ export type IframeEventMap = { showLayer: LayerEvent hideLayer: LayerEvent setProperty: SetPropertyEvent + getDataLayer: undefined } export interface IframeEvent { type: T; @@ -54,7 +55,7 @@ export interface IframeResponseEventMap { buttonClickedEvent: ButtonClickedEvent gameState: GameStateEvent hasPlayerMoved: HasPlayerMovedEvent - hasDataLayerChanged: HasDataLayerChangedEvent + dataLayer: DataLayerEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 48441d34..600ff0a6 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -13,11 +13,10 @@ import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMa import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; import { isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; -import { GameStateEvent } from './Events/GameStateEvent'; -import { deepFreezeClone as deepFreezeClone } from '../utility'; -import { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent'; +import type { GameStateEvent } from './Events/GameStateEvent'; +import type { HasPlayerMovedEvent } from './Events/HasPlayerMovedEvent'; import { Math } from 'phaser'; -import { HasDataLayerChangedEvent } from "./Events/HasDataLayerChangedEvent"; +import type { DataLayerEvent } from "./Events/DataLayerEvent"; @@ -72,11 +71,12 @@ class IframeListener { private readonly _gameStateStream: Subject = new Subject(); public readonly gameStateStream = this._gameStateStream.asObservable(); + private readonly _dataLayerChangeStream: Subject = new Subject(); + public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; - private sendDataLayerChange: boolean = false; init() { window.addEventListener("message", (message: TypedMessageEvent>) => { @@ -138,21 +138,26 @@ class IframeListener { this._gameStateStream.next(); } else if (payload.type == "onPlayerMove") { this.sendPlayerMove = true - } else if (payload.type == "onDataLayerChange") { - this.sendDataLayerChange = true + } else if (payload.type == "getDataLayer") { + this._dataLayerChangeStream.next(); } } - - }, false); } + sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { + this.postMessage({ + 'type' : 'dataLayer', + 'data' : dataLayerEvent + }) + } + sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { this.postMessage({ 'type': 'gameState', - 'data': gameStateEvent //deepFreezeClone(gameStateEvent) + 'data': gameStateEvent }); } @@ -268,15 +273,6 @@ class IframeListener { } } - hasDataLayerChanged(event: HasDataLayerChangedEvent) { - if (this.sendDataLayerChange) { - this.postMessage({ - 'type' : 'hasDataLayerChanged', - 'data' : event - }); - } - } - sendButtonClickedEvent(popupId: number, buttonId: number): void { this.postMessage({ 'type': 'buttonClickedEvent', diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 0c5d804a..f95bfa0f 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,5 +1,7 @@ import type {ITiledMap, ITiledMapLayer, ITiledMapTileLayer} from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; +import {iframeListener} from "../../Api/IframeListener"; +import TilemapLayer = Phaser.Tilemaps.TilemapLayer; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -12,13 +14,14 @@ export class GameMap { private lastProperties = new Map(); private callbacks = new Map>(); public readonly flatLayers: ITiledMapLayer[]; + public readonly phaserLayers: TilemapLayer[] = []; public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array) { this.flatLayers = flattenGroupLayersMap(map); let depth = -2; for (const layer of this.flatLayers) { if(layer.type === 'tilelayer'){ - layer.phaserLayer = phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth); + this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth)); } if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { depth = 10000; @@ -89,6 +92,10 @@ export class GameMap { return properties; } + public getMap(): ITiledMap{ + return this.map; + } + private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map) { const callbacksArray = this.callbacks.get(propName); if (callbacksArray !== undefined) { @@ -127,4 +134,21 @@ export class GameMap { return undefined; } + public findPhaserLayer(layerName: string): TilemapLayer | undefined { + let i = 0; + let found = false; + while (!found && i { + iframeListener.sendDataLayerEvent({data: this.gameMap.getMap()}); + })) + } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { @@ -909,21 +912,21 @@ ${escapedMessage} layer.properties = []; layer.properties.push({name : propertyName, type : typeof propertyValue, value : propertyValue}); return; - } - property.value = propertyValue; + } + property.value = propertyValue; } private setLayerVisibility(layerName: string, visible: boolean): void { - const layer = this.gameMap.findLayer(layerName); - if (layer === undefined) { + const phaserlayer = this.gameMap.findPhaserLayer(layerName); + if (phaserlayer === undefined) { console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); return; } - if(layer.type != "tilelayer"){ + if(phaserlayer.type != "tilelayer"){ console.warn('The layer "' + layerName + '" is not a tilelayer. It can not be show/hide'); return; } - layer.phaserLayer?.setVisible(visible); + phaserlayer.setVisible(visible); this.dirty = true; } @@ -1131,18 +1134,15 @@ ${escapedMessage} this.physics.disableUpdate(); this.physicsEnabled = false; //add collision layer - for (const Layer of this.gameMap.flatLayers) { - if (Layer.type == "tilelayer") { - if (Layer.phaserLayer === undefined) { - throw new Error('phaserLayer of layer "' + Layer.name + '" is undefined'); - } - this.physics.add.collider(this.CurrentPlayer, Layer.phaserLayer, (object1: GameObject, object2: GameObject) => { + for (const phaserLayer of this.gameMap.phaserLayers) { + if (phaserLayer.type == "tilelayer") { + this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - Layer.phaserLayer.setCollisionByProperty({collides: true}); + phaserLayer.setCollisionByProperty({collides: true}); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer - Layer.phaserLayer.renderDebug(this.add.graphics(), { + phaserLayer.renderDebug(this.add.graphics(), { tileColor: null, //non-colliding tiles collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200), // Colliding tiles, faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index b70124b3..2369b86b 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -1,4 +1,3 @@ -import type {HasMovedEvent} from "./GameManager"; import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; import type { PositionInterface } from "../../Connexion/ConnexionModels"; import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; diff --git a/front/src/Phaser/Map/LayersFlattener.ts b/front/src/Phaser/Map/LayersFlattener.ts index 3ea8a449..c5092779 100644 --- a/front/src/Phaser/Map/LayersFlattener.ts +++ b/front/src/Phaser/Map/LayersFlattener.ts @@ -14,9 +14,8 @@ function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLaye if (layer.type === 'group') { flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers); } else { - const layerWithNewName = { ...layer }; - layerWithNewName.name = prefix+layerWithNewName.name; - flatLayers.push(layerWithNewName); + layer.name = prefix+layer.name + flatLayers.push(layer); } } } \ No newline at end of file diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 6734388f..a2fbb70b 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -13,7 +13,7 @@ import type { LayerEvent } from "./Api/Events/LayerEvent"; import type { SetPropertyEvent } from "./Api/Events/setPropertyEvent"; import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent'; import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent'; -import { HasDataLayerChangedEvent, HasDataLayerChangedEventCallback, isHasDataLayerChangedEvent} from "./Api/Events/HasDataLayerChangedEvent"; +import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -40,10 +40,11 @@ interface WorkAdventureApi { getUuid(): Promise; getRoomId(): Promise; getStartLayerName(): Promise; + getNickName(): Promise; onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void - onDataLayerChange(callback: (dataLayerChangedEvent: HasDataLayerChangedEvent) => void): void + getDataLayer(): Promise } declare global { @@ -105,7 +106,7 @@ function getGameState(): Promise { } else { return new Promise((resolver, thrower) => { - stateResolvers.push(resolver); + gameStateResolver.push(resolver); window.parent.postMessage({ type: "getState" }, "*") @@ -113,11 +114,11 @@ function getGameState(): Promise { } } -const stateResolvers: Array<(event: GameStateEvent) => void> = [] +const gameStateResolver: Array<(event: GameStateEvent) => void> = [] +const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] let immutableData: GameStateEvent; const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} -const callbackDataLayerChanged: { [type: string]: HasDataLayerChangedEventCallback | ((arg?: HasDataLayerChangedEvent | never) => void) } = {} function postToParent(content: IframeEvent) { @@ -136,14 +137,21 @@ window.WA = { }) }, - onDataLayerChange(callback: HasDataLayerChangedEventCallback): void { - callbackDataLayerChanged['test'] = callback; - postToParent({ - type : "onDataLayerChange", - data: undefined + getDataLayer(): Promise { + return new Promise((resolver, thrower) => { + dataLayerResolver.push(resolver); + postToParent({ + type: "getDataLayer", + data: undefined + }) }) }, + getNickName() { + return getGameState().then((res) => { + return res.nickname; + }) + }, getMapUrl() { return getGameState().then((res) => { @@ -345,14 +353,16 @@ window.addEventListener('message', message => { callback(popup); } } else if (payload.type == "gameState" && isGameStateEvent(payloadData)) { - stateResolvers.forEach(resolver => { + gameStateResolver.forEach(resolver => { resolver(payloadData); }) immutableData = payloadData; } else if (payload.type == "hasPlayerMoved" && isHasPlayerMovedEvent(payloadData) && playerUuid) { callbackPlayerMoved[playerUuid](payloadData) - } else if (payload.type == "hasDataLayerChanged" && isHasDataLayerChangedEvent(payloadData)) { - callbackDataLayerChanged['test'](payloadData) + } else if (payload.type == "dataLayer" && isDataLayerEvent(payloadData)) { + dataLayerResolver.forEach(resolver => { + resolver(payloadData); + }) } } diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js index f3ac255a..c857d783 100644 --- a/maps/tests/Metadata/script.js +++ b/maps/tests/Metadata/script.js @@ -1,9 +1,9 @@ -WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); +/*WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); WA.getUuid().then((uuid) => {console.log('Uuid : ',uuid)}); -WA.getRoomId().then((roomId) => console.log('roomID : ',roomId)); - -WA.listenPositionPlayer(console.log); - +WA.getRoomId().then((roomId) => console.log('roomID : ',roomId));*/ +//WA.onPlayerMove(console.log); +WA.setProperty('metadata', 'openWebsite', 'https://fr.wikipedia.org/'); +WA.getDataLayer().then((data) => {console.log('data 1 : ', data)}); \ No newline at end of file From b509471140c10126e5c6864f98ed7e22e4543b10 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 18 May 2021 17:05:16 +0200 Subject: [PATCH 24/87] documentation documentation of onPlayerMove documentation of getMap documentation of getGameState --- docs/maps/api-reference.md | 94 ++++++++++++++++++++++++++++++++++++++ front/src/iframe_api.ts | 34 ++++++++------ 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 6e98dfb5..8eb00397 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -260,7 +260,101 @@ WA.setProperty(layerName : string, propertyName : string, propertyValue : string Set the value of the "propertyName" property of the layer "layerName" at "propertyValue". If the property doesn't exist, create the property "propertyName" and set the value of the property at "propertyValue". +Example : + ```javascript WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` +### Listen player movement + +``` +onPlayerMove(callback: HasPlayerMovedEventCallback): void; +``` +Listens to the movement of the current user and calls the callback. Send a event when current user stop moving, change direction and every 200ms when moving in the same direction. + +The event has the following attributes : +* **moving (boolean):** **true** when the current player is moving, **false** otherwise. +* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. +* **x (number):** coordinate X of the current player. +* **y (number):** coordinate Y of the current player. + +**callback:** the function that will be called when the current player is moving. It contains the event. + +Exemple : +```javascript +WA.onPlayerMove(console.log); +``` + +### Getting the map + +``` +getMap(): Promise +``` + +Return a promise of an ITiledMap that contains the JSON file of the map plus the property set by a script. + +Example : +```javascript +WA.getMap().then((data) => console.log(data.layers)); +``` + +### Getting the url of the JSON file map + +``` +getMapUrl(): Promise +``` + +Return a promise of the url of the JSON file map. + +Example : +```javascript +WA.getMapUrl().then((mapUrl) => {console.log(mapUrl)}); +``` + +### Getting the roomID +``` +getRoomId(): Promise +``` +Return a promise of the ID of the current room. + +Example : +```javascript +WA.getRoomId().then((roomId) => console.log(roomId)); +``` + +### Getting the UUID of the current user +``` +getUuid(): Promise +``` +Return a promise of the ID of the current user. + +Example : +```javascript +WA.getUuid().then((uuid) => {console.log(uuid)}); +``` + +### Getting the nickname of the current user +``` +getNickName(): Promise +``` +Return a promise of the nickname of the current user. + +Example : +```javascript +WA.getNickName().then((nickname) => {console.log(nickname)}); +``` + +### Getting the name of the layer where the current user started (if other than start) +``` +getStartLayerName(): Promise +``` +Return a promise of the name of the layer where the current user started if the name is different than "start". + +Example : +```javascript +WA.getStartLayerName().then((starLayerName) => {console.log(starLayerName)}); +``` + + + diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index a2fbb70b..4fdb0a03 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -14,6 +14,7 @@ import type { SetPropertyEvent } from "./Api/Events/setPropertyEvent"; import { GameStateEvent, isGameStateEvent } from './Api/Events/GameStateEvent'; import { HasPlayerMovedEvent, HasPlayerMovedEventCallback, isHasPlayerMovedEvent } from './Api/Events/HasPlayerMovedEvent'; import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; +import type {ITiledMap} from "./Phaser/Map/ITiledMap"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -44,7 +45,7 @@ interface WorkAdventureApi { onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void - getDataLayer(): Promise + getMap(): Promise } declare global { @@ -114,6 +115,16 @@ function getGameState(): Promise { } } +function getDataLayer(): Promise { + return new Promise((resolver, thrower) => { + dataLayerResolver.push(resolver); + postToParent({ + type: "getDataLayer", + data: undefined + }) + }) +} + const gameStateResolver: Array<(event: GameStateEvent) => void> = [] const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] let immutableData: GameStateEvent; @@ -137,41 +148,38 @@ window.WA = { }) }, - getDataLayer(): Promise { - return new Promise((resolver, thrower) => { - dataLayerResolver.push(resolver); - postToParent({ - type: "getDataLayer", - data: undefined - }) + + getMap(): Promise { + return getDataLayer().then((res) => { + return res.data as ITiledMap; }) }, - getNickName() { + getNickName(): Promise { return getGameState().then((res) => { return res.nickname; }) }, - getMapUrl() { + getMapUrl(): Promise { return getGameState().then((res) => { return res.mapUrl; }) }, - getUuid() { + getUuid(): Promise { return getGameState().then((res) => { return res.uuid; }) }, - getRoomId() { + getRoomId(): Promise { return getGameState().then((res) => { return res.roomId; }) }, - getStartLayerName() { + getStartLayerName(): Promise { return getGameState().then((res) => { return res.startLayerName; }) From 96545c618a3a6fb71db728017ce868d80e28cf01 Mon Sep 17 00:00:00 2001 From: GRL Date: Thu, 20 May 2021 08:58:05 +0200 Subject: [PATCH 25/87] Adding maps for test metadata Documentation of metadata functions/methods --- docs/maps/api-reference.md | 14 +- maps/tests/Metadata/customMenu.html | 15 + maps/tests/Metadata/customMenu.json | 279 ++++++++++++++++++ maps/tests/Metadata/floortileset.png | Bin 0 -> 81856 bytes maps/tests/Metadata/getGameState.html | 42 +++ maps/tests/Metadata/getGameState.json | 279 ++++++++++++++++++ maps/tests/Metadata/getGameState2.html | 40 +++ maps/tests/Metadata/getGameState2.json | 273 +++++++++++++++++ maps/tests/Metadata/playerMove.html | 12 + maps/tests/Metadata/playerMove.json | 254 ++++++++++++++++ maps/tests/Metadata/script.js | 9 - maps/tests/Metadata/setProperty.html | 12 + maps/tests/Metadata/setProperty.json | 266 +++++++++++++++++ maps/tests/Metadata/showHideLayer.html | 21 ++ .../Metadata/{map.json => showHideLayer.json} | 84 ++++-- maps/tests/iframe.html | 30 +- 16 files changed, 1571 insertions(+), 59 deletions(-) create mode 100644 maps/tests/Metadata/customMenu.html create mode 100644 maps/tests/Metadata/customMenu.json create mode 100644 maps/tests/Metadata/floortileset.png create mode 100644 maps/tests/Metadata/getGameState.html create mode 100644 maps/tests/Metadata/getGameState.json create mode 100644 maps/tests/Metadata/getGameState2.html create mode 100644 maps/tests/Metadata/getGameState2.json create mode 100644 maps/tests/Metadata/playerMove.html create mode 100644 maps/tests/Metadata/playerMove.json delete mode 100644 maps/tests/Metadata/script.js create mode 100644 maps/tests/Metadata/setProperty.html create mode 100644 maps/tests/Metadata/setProperty.json create mode 100644 maps/tests/Metadata/showHideLayer.html rename maps/tests/Metadata/{map.json => showHideLayer.json} (70%) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 8eb00397..01d3e636 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -356,5 +356,17 @@ Example : WA.getStartLayerName().then((starLayerName) => {console.log(starLayerName)}); ``` +### Add a custom menu +``` +registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) +``` +Add a custom menu named "commandDescriptor" in the menu that call the callback when clicked. - +Example : +```javascript +let chatbotEnabled = false +WA.registerMenuCommand('help', () => { + chatbotEnabled = true; + WA.onChatMessage ... +}); +``` diff --git a/maps/tests/Metadata/customMenu.html b/maps/tests/Metadata/customMenu.html new file mode 100644 index 00000000..59f579ba --- /dev/null +++ b/maps/tests/Metadata/customMenu.html @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/maps/tests/Metadata/customMenu.json b/maps/tests/Metadata/customMenu.json new file mode 100644 index 00000000..49840d0b --- /dev/null +++ b/maps/tests/Metadata/customMenu.json @@ -0,0 +1,279 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":6, + "name":"exit", + "opacity":1, + "properties":[ + { + "name":"exitUrl", + "type":"string", + "value":"showHideLayer.json" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"customMenu.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":217.142414860681, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass, an iframe open.\nResult : \nOpen the menu, a new sub-menu is displayed.\n\nTest : \nExit the grass\nResult : \nOpen the menu, the submenu has disappeared.\n\nTest : \nClick on the 'HELP' menu.\nResult : \nChat open and a 'HELP' message is displayed.\n\nTest : \nWalk on the red tile then open the menu.\nResult : \nYou have exit the room to another room, the submenu has disappeared.\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":15.1244925229277, + "y":103.029937496349 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":7, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/floortileset.png b/maps/tests/Metadata/floortileset.png new file mode 100644 index 0000000000000000000000000000000000000000..82de9c908ad4a15eeea48995f758f1f0d95a00ac GIT binary patch literal 81856 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·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 m14-}S`skTl&cMLneE1(^FfMcz0-ikp0000 + + + + + + +

+ + +
+ + +
+ + + + \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState.json b/maps/tests/Metadata/getGameState.json new file mode 100644 index 00000000..a005ee8a --- /dev/null +++ b/maps/tests/Metadata/getGameState.json @@ -0,0 +1,279 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"getGameState.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":8, + "name":"exit", + "opacity":1, + "properties":[ + { + "name":"exitUrl", + "type":"string", + "value":"getGameState2.json#HereYouAppear" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":218.263975699515, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Start the test : \nWalk on the grass, an iframe open.\n\nTest : \nClick on the 'nickname' button.\nResult : \nYour nickname appears.\n\nTest : \nClick on the 'roomID' button.\nResult : \nAn ID appears.\n\nTest : \nClick on the 'UUID' button.\nResult : \nAn ID appears.\n\nFinally : \nWalk on the red tiles to continue the testing.\n\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":14.750638909983, + "y":101.908376657515 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":9, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState2.html b/maps/tests/Metadata/getGameState2.html new file mode 100644 index 00000000..e8529617 --- /dev/null +++ b/maps/tests/Metadata/getGameState2.html @@ -0,0 +1,40 @@ + + + + + + + +
+ + +
+ + +
+ + + + \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState2.json b/maps/tests/Metadata/getGameState2.json new file mode 100644 index 00000000..04127918 --- /dev/null +++ b/maps/tests/Metadata/getGameState2.json @@ -0,0 +1,273 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":9, + "name":"HereYouAppear", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"getGameState2.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":200.31900227817, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Start the test : \nWalk on the grass, an iframe open.\n\nTest : \nClick on the 'startLayer' button.\nResult : \nThe name of the layer where you start appears. (only work when the start layer is not 'start')\n\nTest : \nClick on the 'mapUrl' button.\nResult : \nThe url of the JSON file of the map is displayed in the console.log().\n\nTest : \nClick on the 'Map' button.\nResult : \nThe JSON file map appears.\n\n\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":14.750638909983, + "y":119.85335007886 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":10, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/playerMove.html b/maps/tests/Metadata/playerMove.html new file mode 100644 index 00000000..3fecf576 --- /dev/null +++ b/maps/tests/Metadata/playerMove.html @@ -0,0 +1,12 @@ + + + + + + +
+ + + \ No newline at end of file diff --git a/maps/tests/Metadata/playerMove.json b/maps/tests/Metadata/playerMove.json new file mode 100644 index 00000000..db590b05 --- /dev/null +++ b/maps/tests/Metadata/playerMove.json @@ -0,0 +1,254 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"playerMove.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":159.195104854255, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass, an iframe open.\nResult : \nIf you move on the grass, your movement will be displayed in the console.log(). \nYour movement appears according to the following rules : \n - When you stop (the moving attribute will be false)\n - When you change direction (the direction attribute will change value)\n - Every 200ms if you keep moving in the same direction.\n\nMovement are represented by the following attributes : \n - moving : if you are moving or not.\n - direction : the direction where you are moving into\n - X and Y coordinates : Place of your character in the room.\n\n\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":14.750638909983, + "y":160.977247502775 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":10, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js deleted file mode 100644 index c857d783..00000000 --- a/maps/tests/Metadata/script.js +++ /dev/null @@ -1,9 +0,0 @@ - - -/*WA.getMapUrl().then((map) => {console.log('mapUrl : ', map)}); -WA.getUuid().then((uuid) => {console.log('Uuid : ',uuid)}); -WA.getRoomId().then((roomId) => console.log('roomID : ',roomId));*/ - -//WA.onPlayerMove(console.log); -WA.setProperty('metadata', 'openWebsite', 'https://fr.wikipedia.org/'); -WA.getDataLayer().then((data) => {console.log('data 1 : ', data)}); \ No newline at end of file diff --git a/maps/tests/Metadata/setProperty.html b/maps/tests/Metadata/setProperty.html new file mode 100644 index 00000000..06b029da --- /dev/null +++ b/maps/tests/Metadata/setProperty.html @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/maps/tests/Metadata/setProperty.json b/maps/tests/Metadata/setProperty.json new file mode 100644 index 00000000..06addc2f --- /dev/null +++ b/maps/tests/Metadata/setProperty.json @@ -0,0 +1,266 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"setProperty.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 101, 101, 101, 101, 0, 0, 0, 0, 0, 101, 101, 101, 101, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":7, + "name":"iframeTest", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":157.325836789532, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the red tiles.\nResult :\nNothing happens.\n\nTest : \nWalk on the grass, an iframe open. Then walk on the red tiles.\nResult : \nAn iframe of Wikipedia open.\n\nTest : \nWalk on the grass again.\nResult : \nAn iframe of Wikipedia open.\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":15.1244925229277, + "y":162.846515567498 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":8, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/showHideLayer.html b/maps/tests/Metadata/showHideLayer.html new file mode 100644 index 00000000..391ec449 --- /dev/null +++ b/maps/tests/Metadata/showHideLayer.html @@ -0,0 +1,21 @@ + + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/maps/tests/Metadata/map.json b/maps/tests/Metadata/showHideLayer.json similarity index 70% rename from maps/tests/Metadata/map.json rename to maps/tests/Metadata/showHideLayer.json index 8967ed02..df61a655 100644 --- a/maps/tests/Metadata/map.json +++ b/maps/tests/Metadata/showHideLayer.json @@ -3,7 +3,7 @@ "infinite":false, "layers":[ { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":10, "id":1, "name":"start", @@ -15,7 +15,7 @@ "y":0 }, { - "data":[46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 33, 34, 34, 34, 34, 34, 34, 35, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 41, 42, 42, 42, 42, 42, 42, 43, 46, 46, 49, 50, 50, 50, 50, 50, 50, 51, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46], + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], "height":10, "id":2, "name":"bottom", @@ -27,11 +27,34 @@ "y":0 }, { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 52, 52, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "data":[22, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 22, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":6, + "name":"crystal", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":10, "id":4, "name":"metadata", "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"showHideLayer.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], "type":"tilelayer", "visible":true, "width":10, @@ -42,34 +65,34 @@ "draworder":"topdown", "id":5, "name":"floorLayer", - "objects":[], + "objects":[ + { + "height":191.346515567498, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass, an iframe open, uncheck the checkbox.\nResult : \nCrystals disappeared.\n\nTest : \nCheck the checkbox\nResult : \nCrystals appear.", + "wrap":true + }, + "type":"", + "visible":true, + "width":306.219266604358, + "x":14.0029316840937, + "y":128.078129563643 + }], "opacity":1, "type":"objectgroup", "visible":true, "x":0, "y":0 - }, - { - "data":[1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19], - "height":10, - "id":3, - "name":"wall", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 }], - "nextlayerid":6, - "nextobjectid":1, + "nextlayerid":7, + "nextobjectid":2, "orientation":"orthogonal", - "properties":[ - { - "name":"script", - "type":"string", - "value":"script.js" - }], "renderorder":"right-down", "tiledversion":"1.4.3", "tileheight":32, @@ -222,6 +245,19 @@ }] }], "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 }], "tilewidth":32, "type":"map", diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index b46b8c32..aa8e55ec 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -12,31 +12,11 @@
-
- -
- From 2f9cc393a79ff6cc23864372f7e961d6bd262544 Mon Sep 17 00:00:00 2001 From: GRL Date: Thu, 20 May 2021 10:57:36 +0200 Subject: [PATCH 26/87] Implementation of getTag of the current user documentation of getTag Adding map for test of getTag --- docs/maps/api-reference.md | 15 ++ front/src/Api/Events/IframeEvent.ts | 4 +- front/src/Api/Events/TagEvent.ts | 10 + front/src/Api/IframeListener.ts | 14 ++ front/src/Connexion/RoomConnection.ts | 7 + front/src/Phaser/Game/GameScene.ts | 7 + front/src/iframe_api.ts | 27 ++- maps/tests/Metadata/TagList.html | 19 ++ maps/tests/Metadata/TagList.json | 254 ++++++++++++++++++++++++++ 9 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 front/src/Api/Events/TagEvent.ts create mode 100644 maps/tests/Metadata/TagList.html create mode 100644 maps/tests/Metadata/TagList.json diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 01d3e636..889ed3ac 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -370,3 +370,18 @@ WA.registerMenuCommand('help', () => { WA.onChatMessage ... }); ``` + +### Getting the list of tags of the current user +``` +getTagUser(): Promise +``` + +Return the list of all the tags that has the current user. If the current user has no tag, return an empty list. If there is no connection with the room, return nothing. + +Example : +```javascript +WA.getTagUser().then((tagList) => { + ... +}); +``` + diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 8383cfbd..114cbb90 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,6 +15,7 @@ import type { UserInputChatEvent } from './UserInputChatEvent'; import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; +import type { TagEvent } from "./TagEvent"; export interface TypedMessageEvent extends MessageEvent { data: T @@ -36,11 +37,11 @@ export type IframeEventMap = { displayBubble: null removeBubble: null onPlayerMove: undefined - onDataLayerChange: undefined showLayer: LayerEvent hideLayer: LayerEvent setProperty: SetPropertyEvent getDataLayer: undefined + getTag: undefined } export interface IframeEvent { type: T; @@ -60,6 +61,7 @@ export interface IframeResponseEventMap { hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent + tagList: TagEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/TagEvent.ts b/front/src/Api/Events/TagEvent.ts new file mode 100644 index 00000000..66665403 --- /dev/null +++ b/front/src/Api/Events/TagEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isTagEvent = + new tg.IsInterface().withProperties({ + list: tg.isArray(tg.isString), + }).get(); +/** + * A message sent from the iFrame to the game to show/hide a layer. + */ +export type TagEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 07246333..35ef6341 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -19,6 +19,7 @@ import { Math } from 'phaser'; import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; +import type { TagEvent } from "./Events/TagEvent"; /** @@ -77,6 +78,10 @@ class IframeListener { private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); + + private readonly _tagListStream: Subject = new Subject(); + public readonly tagListStream = this._tagListStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -145,12 +150,21 @@ class IframeListener { this._dataLayerChangeStream.next(); } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { this._registerMenuCommandStream.next(payload.data.menutItem) + } else if (payload.type == "getTag") { + this._tagListStream.next(); } } }, false); } + sendUserTagList(tagList: TagEvent){ + this.postMessage({ + 'type' : 'tagList', + 'data' : tagList + }) + } + sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { this.postMessage({ 'type' : 'dataLayer', diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 6b2c63af..1cb4a97d 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -598,4 +598,11 @@ export class RoomConnection implements RoomConnection { public isAdmin(): boolean { return this.hasTag('admin'); } + + public getAllTag() : string[] { + this.tags.push('TEST'); + this.tags.push('TEST 2'); + this.tags.push('TEST 3'); + return this.tags; + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9150b4c1..dee5eb53 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -903,6 +903,13 @@ ${escapedMessage} iframeListener.sendDataLayerEvent({data: this.gameMap.getMap()}); })) + this.iframeSubscriptionList.push(iframeListener.tagListStream.subscribe(()=> { + if (this.connection === undefined) { + return; + } + iframeListener.sendUserTagList({list: this.connection.getAllTag()}); + })) + } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 00977157..517248ed 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -17,6 +17,7 @@ import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; import type { ITiledMap } from "./Phaser/Map/ITiledMap"; import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; +import {TagEvent, isTagEvent} from "./Api/Events/TagEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -45,10 +46,10 @@ interface WorkAdventureApi { getRoomId(): Promise; getStartLayerName(): Promise; getNickName(): Promise; - + getTagUser(): Promise; + getMap(): Promise onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void - getMap(): Promise } declare global { @@ -128,8 +129,19 @@ function getDataLayer(): Promise { }) } +function getTag(): Promise { + return new Promise((resolver, thrower) => { + tagResolver.push((resolver)); + postToParent({ + type: "getTag", + data: undefined + }) + }) +} + const gameStateResolver: Array<(event: GameStateEvent) => void> = [] const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] +const tagResolver: Array<(event : TagEvent) => void> = [] let immutableData: GameStateEvent; const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} @@ -151,6 +163,11 @@ window.WA = { }) }, + getTagUser(): Promise { + return getTag().then((res) => { + return res.list; + }) + }, getMap(): Promise { return getDataLayer().then((res) => { @@ -389,6 +406,12 @@ window.addEventListener('message', message => { if (callback) { callback(payload.data.menuItem) } + } else { + if (payload.type == "tagList" && isTagEvent(payloadData)) { + tagResolver.forEach(resolver => { + resolver(payloadData); + }) + } } } diff --git a/maps/tests/Metadata/TagList.html b/maps/tests/Metadata/TagList.html new file mode 100644 index 00000000..73bdc368 --- /dev/null +++ b/maps/tests/Metadata/TagList.html @@ -0,0 +1,19 @@ + + + + + + + + +
+ + \ No newline at end of file diff --git a/maps/tests/Metadata/TagList.json b/maps/tests/Metadata/TagList.json new file mode 100644 index 00000000..cced49a3 --- /dev/null +++ b/maps/tests/Metadata/TagList.json @@ -0,0 +1,254 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":4, + "name":"metadata", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"TagList.html" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[ + { + "height":131.903791109293, + "id":1, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":9, + "text":"Test : \nWalk on the grass, an iframe open, click on the 'Get Tag List' button.\nResult : \nThe list of the tag is displayed in the iframe.\n\n\n", + "wrap":true + }, + "type":"", + "visible":true, + "width":305.097705765524, + "x":14.750638909983, + "y":188.268561247737 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":10, + "nextobjectid":2, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file From 3506063e65a2b8f62c3c4faec897bf1dffa2e62b Mon Sep 17 00:00:00 2001 From: GRL Date: Thu, 20 May 2021 17:09:10 +0200 Subject: [PATCH 27/87] first step on loading a tileset from a script --- front/src/Api/Events/IframeEvent.ts | 2 + front/src/Api/Events/TilesetEvent.ts | 15 ++ front/src/Api/IframeListener.ts | 8 +- front/src/Phaser/Game/GameMap.ts | 8 +- front/src/Phaser/Game/GameScene.ts | 9 +- front/src/iframe_api.ts | 16 ++ maps/tests/Metadata/ScriptMap.json | 219 +++++++++++++++++++++++++++ maps/tests/Metadata/script.js | 1 + maps/tests/iframe.html | 1 + 9 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 front/src/Api/Events/TilesetEvent.ts create mode 100644 maps/tests/Metadata/ScriptMap.json create mode 100644 maps/tests/Metadata/script.js diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 114cbb90..1ee7d1fb 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -16,6 +16,7 @@ import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { TagEvent } from "./TagEvent"; +import type { TilesetEvent } from "./TilesetEvent"; export interface TypedMessageEvent extends MessageEvent { data: T @@ -42,6 +43,7 @@ export type IframeEventMap = { setProperty: SetPropertyEvent getDataLayer: undefined getTag: undefined + tilsetEvent: TilesetEvent } export interface IframeEvent { type: T; diff --git a/front/src/Api/Events/TilesetEvent.ts b/front/src/Api/Events/TilesetEvent.ts new file mode 100644 index 00000000..eab33bf7 --- /dev/null +++ b/front/src/Api/Events/TilesetEvent.ts @@ -0,0 +1,15 @@ +import * as tg from "generic-type-guard"; + +export const isTilesetEvent = + new tg.IsInterface().withProperties({ + name : tg.isString, + imgUrl : tg.isString, + tilewidth : tg.isNumber, + tileheight : tg.isNumber, + margin : tg.isNumber, + spacing : tg.isNumber, + }).get(); +/** + * A message sent from the iFrame to the game to show/hide a layer. + */ +export type TilesetEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 35ef6341..8af0949f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -20,6 +20,7 @@ import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; import type { TagEvent } from "./Events/TagEvent"; +import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; /** @@ -79,9 +80,12 @@ class IframeListener { private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); - private readonly _tagListStream: Subject = new Subject(); + private readonly _tagListStream: Subject = new Subject(); public readonly tagListStream = this._tagListStream.asObservable(); + private readonly _tilesetLoaderStream: Subject = new Subject(); + public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable(); + private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -152,6 +156,8 @@ class IframeListener { this._registerMenuCommandStream.next(payload.data.menutItem) } else if (payload.type == "getTag") { this._tagListStream.next(); + } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { + this._tilesetLoaderStream.next(payload.data); } } }, false); diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 24ca60c7..d63a67e0 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,6 +1,5 @@ import type {ITiledMap, ITiledMapLayer, ITiledMapTileLayer} from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; -import {iframeListener} from "../../Api/IframeListener"; import TilemapLayer = Phaser.Tilemaps.TilemapLayer; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -151,4 +150,11 @@ export class GameMap { return undefined; } + public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { + console.log('Add'); + for (const phaserLayer of this.phaserLayers) { + phaserLayer.tileset.push(terrain); + } + } + } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index dee5eb53..120bb303 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -500,7 +500,7 @@ export class GameScene extends DirtyScene implements CenterListener { if (!this.room.isDisconnected()) { this.connect(); } - + console.log('display'); document.addEventListener('visibilitychange', this.onVisibilityChangeCallback); } @@ -910,6 +910,13 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) + this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { + //this.load.tilemapTiledJSON('logo', tileset.imgUrl); + this.load.image('logo', tileset.imgUrl); + this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); + this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); + })) + } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 517248ed..5a3336a4 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -18,6 +18,7 @@ import type { ITiledMap } from "./Phaser/Map/ITiledMap"; import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; import {TagEvent, isTagEvent} from "./Api/Events/TagEvent"; +import type { TilesetEvent } from "./Api/Events/TilesetEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -48,6 +49,7 @@ interface WorkAdventureApi { getNickName(): Promise; getTagUser(): Promise; getMap(): Promise + loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void } @@ -163,6 +165,20 @@ window.WA = { }) }, + loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { + postToParent({ + type: "tilsetEvent", + data: { + name: name, + imgUrl: imgUrl, + tilewidth: tilewidth, + tileheight: tileheight, + margin: margin, + spacing: spacing + } as TilesetEvent + }) + }, + getTagUser(): Promise { return getTag().then((res) => { return res.list; diff --git a/maps/tests/Metadata/ScriptMap.json b/maps/tests/Metadata/ScriptMap.json new file mode 100644 index 00000000..93972a73 --- /dev/null +++ b/maps/tests/Metadata/ScriptMap.json @@ -0,0 +1,219 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":1, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "height":10, + "id":2, + "name":"bottom", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":5, + "name":"floorLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":10, + "nextobjectid":2, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.4.3", + "tileheight":32, + "tilesets":[ + { + "columns":8, + "firstgid":1, + "image":"tileset_dungeon.png", + "imageheight":256, + "imagewidth":256, + "margin":0, + "name":"TDungeon", + "spacing":0, + "tilecount":64, + "tileheight":32, + "tiles":[ + { + "id":0, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":11, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }, + { + "columns":8, + "firstgid":65, + "image":"floortileset.png", + "imageheight":288, + "imagewidth":256, + "margin":0, + "name":"Floor", + "spacing":0, + "tilecount":72, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.4, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js new file mode 100644 index 00000000..d04d7952 --- /dev/null +++ b/maps/tests/Metadata/script.js @@ -0,0 +1 @@ +console.log('script chargé !!!!!'); \ No newline at end of file diff --git a/maps/tests/iframe.html b/maps/tests/iframe.html index aa8e55ec..e0ba05d6 100644 --- a/maps/tests/iframe.html +++ b/maps/tests/iframe.html @@ -17,6 +17,7 @@ chatDiv.innerText = message; document.getElementById('chatSent').append(chatDiv); })); + WA.loadTileset('TEST', 'https://gparant.github.io/tcm-client/TCM/paris-map/tileset1.png', 32, 32, 0, 0); From 1110f4fb7f6132a07c808ccc2522b1ce524420af Mon Sep 17 00:00:00 2001 From: GRL Date: Fri, 21 May 2021 16:24:48 +0200 Subject: [PATCH 28/87] Revert "Merge branch 'update-game-tiles' into metadataScriptingApi" This reverts commit 796a9418d3b6c356c5c25bfbc4503207b08572c4, reversing changes made to 3506063e65a2b8f62c3c4faec897bf1dffa2e62b. --- front/src/Api/Events/IframeEvent.ts | 4 +-- front/src/Api/Events/UpdateTileEvent.ts | 15 --------- front/src/Api/IframeListener.ts | 14 ++++---- front/src/Phaser/Game/GameScene.ts | 45 +++---------------------- front/src/Phaser/Map/ITiledMap.ts | 21 +++++------- front/src/iframe_api.ts | 22 +++++++++++- 6 files changed, 42 insertions(+), 79 deletions(-) delete mode 100644 front/src/Api/Events/UpdateTileEvent.ts diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 6a76f870..1ee7d1fb 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -17,7 +17,6 @@ import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { TagEvent } from "./TagEvent"; import type { TilesetEvent } from "./TilesetEvent"; -import type { UpdateTileEvent } from "./UpdateTileEvent"; export interface TypedMessageEvent extends MessageEvent { data: T @@ -44,8 +43,7 @@ export type IframeEventMap = { setProperty: SetPropertyEvent getDataLayer: undefined getTag: undefined - tilesetEvent: TilesetEvent - updateTileEvent: UpdateTileEvent + tilsetEvent: TilesetEvent } export interface IframeEvent { type: T; diff --git a/front/src/Api/Events/UpdateTileEvent.ts b/front/src/Api/Events/UpdateTileEvent.ts deleted file mode 100644 index 5817622c..00000000 --- a/front/src/Api/Events/UpdateTileEvent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as tg from "generic-type-guard"; - - -export const isUpdateTileEvent = tg.isArray( - new tg.IsInterface().withProperties({ - x: tg.isNumber, - y: tg.isNumber, - tile: tg.isUnion(tg.isNumber, tg.isString), - layer: tg.isString - }).get() -); -/** - * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. - */ -export type UpdateTileEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 2406e92d..8af0949f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -21,7 +21,6 @@ import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; import type { TagEvent } from "./Events/TagEvent"; import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; -import { isUpdateTileEvent, UpdateTileEvent } from './Events/UpdateTileEvent'; /** @@ -36,6 +35,12 @@ class IframeListener { private readonly _openPopupStream: Subject = new Subject(); public readonly openPopupStream = this._openPopupStream.asObservable(); + private readonly _openTabStream: Subject = new Subject(); + public readonly openTabStream = this._openTabStream.asObservable(); + + private readonly _goToPageStream: Subject = new Subject(); + public readonly goToPageStream = this._goToPageStream.asObservable(); + private readonly _openCoWebSiteStream: Subject = new Subject(); public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable(); @@ -81,9 +86,6 @@ class IframeListener { private readonly _tilesetLoaderStream: Subject = new Subject(); public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable(); - private readonly _updateTileStream: Subject = new Subject(); - public readonly updateTileStream = this._updateTileStream.asObservable(); - private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -154,10 +156,8 @@ class IframeListener { this._registerMenuCommandStream.next(payload.data.menutItem) } else if (payload.type == "getTag") { this._tagListStream.next(); - } else if (payload.type == "tilesetEvent" && isTilesetEvent(payload.data)) { + } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { this._tilesetLoaderStream.next(payload.data); - } else if (payload.type == "updateTileEvent" && isUpdateTileEvent(payload.data)) { - this._updateTileStream.next(payload.data) } } }, false); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 33013454..120bb303 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,4 @@ -import { gameManager } from "./GameManager"; +import {gameManager} from "./GameManager"; import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -80,7 +80,6 @@ import CanvasTexture = Phaser.Textures.CanvasTexture; import GameObject = Phaser.GameObjects.GameObject; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import DOMElement = Phaser.GameObjects.DOMElement; -import EVENT_TYPE = Phaser.Scenes.Events import type { Subscription } from "rxjs"; import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; @@ -186,7 +185,7 @@ export class GameScene extends DirtyScene implements CenterListener { private characterLayers!: string[]; private companion!: string | null; private messageSubscription: Subscription | null = null; - private popUpElements: Map = new Map(); + private popUpElements : Map = new Map(); private originalMapUrl: string | undefined; private pinchManager: PinchManager | undefined; private physicsEnabled: boolean = true; @@ -911,33 +910,12 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) -/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); this.load.image('logo', tileset.imgUrl); this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); })) -*/ - - this.iframeSubscriptionList.push(iframeListener.updateTileStream.subscribe(event => { - for (const eventTile of event) { - const layer = this.gameMap.findPhaserLayer(eventTile.layer); - if (layer) { - const tile = layer.getTileAt(eventTile.x, eventTile.y) - if (typeof eventTile.tile == "string") { - const tileIndex = this.getIndexForTileType(eventTile.tile); - if (tileIndex) { - tile.index = tileIndex - } else { - return - } - } else { - tile.index = eventTile.tile - } - } - } - })) } @@ -967,19 +945,6 @@ ${escapedMessage} } - private getIndexForTileType(tileType: string): number | null { - for (const tileset of this.mapFile.tilesets) { - if (tileset.tiles) { - for (const tilesetTile of tileset.tiles) { - if (tilesetTile.type == tileType) { - return tileset.firstgid + tilesetTile.id - } - } - } - } - return null - } - private getMapDirUrl(): string { return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); } @@ -987,8 +952,8 @@ ${escapedMessage} private onMapExit(exitKey: string) { if (this.mapTransitioning) return; this.mapTransitioning = true; - const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); - if (!roomId) throw new Error('Could not find the room from its exit key: ' + exitKey); + const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); + if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); urlManager.pushStartLayerNameToUrl(hash); const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene menuScene.reset() @@ -1190,7 +1155,7 @@ ${escapedMessage} this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - phaserLayer.setCollisionByProperty({ collides: true }); + phaserLayer.setCollisionByProperty({collides: true}); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer phaserLayer.renderDebug(this.add.graphics(), { diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index 2f5d45bc..d381e9d4 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -36,7 +36,7 @@ export interface ITiledMap { export interface ITiledMapLayerProperty { name: string; type: string; - value: string | boolean | number | undefined; + value: string|boolean|number|undefined; } /*export interface ITiledMapLayerBooleanProperty { @@ -65,7 +65,7 @@ export interface ITiledMapGroupLayer { export interface ITiledMapTileLayer { id?: number, - data: number[] | string; + data: number[]|string; height: number; name: string; opacity: number; @@ -117,7 +117,7 @@ export interface ITiledMapObject { gid: number; height: number; name: string; - properties: { [key: string]: string }; + properties: {[key: string]: string}; rotation: number; type: string; visible: boolean; @@ -133,12 +133,12 @@ export interface ITiledMapObject { /** * Polygon points */ - polygon: { x: number, y: number }[]; + polygon: {x: number, y: number}[]; /** * Polyline points */ - polyline: { x: number, y: number }[]; + polyline: {x: number, y: number}[]; text?: ITiledText } @@ -152,7 +152,7 @@ export interface ITiledText { underline?: boolean, italic?: boolean, strikeout?: boolean, - halign?: "center" | "right" | "justify" | "left" + halign?: "center"|"right"|"justify"|"left" } export interface ITiledTileSet { @@ -163,14 +163,14 @@ export interface ITiledTileSet { imagewidth: number; margin: number; name: string; - properties: { [key: string]: string }; + properties: {[key: string]: string}; spacing: number; tilecount: number; tileheight: number; tilewidth: number; transparentcolor: string; terrains: ITiledMapTerrain[]; - tiles: Array; + tiles: {[key: string]: { terrain: number[] }}; /** * Refers to external tileset file (should be JSON) @@ -178,11 +178,6 @@ export interface ITiledTileSet { source: string; } -export interface ITile { - id: number, - type?: string -} - export interface ITiledMapTerrain { name: string; tile: number; diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index f253c48d..5a3336a4 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -26,6 +26,8 @@ interface WorkAdventureApi { onEnterZone(name: string, callback: () => void): void; onLeaveZone(name: string, callback: () => void): void; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup; + openTab(url: string): void; + goToPage(url: string): void; openCoWebSite(url: string): void; closeCoWebSite(): void; disablePlayerControls() : void; @@ -165,7 +167,7 @@ window.WA = { loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { postToParent({ - type: "updateTileEvent", + type: "tilsetEvent", data: { name: name, imgUrl: imgUrl, @@ -274,6 +276,24 @@ window.WA = { window.parent.postMessage({ 'type': 'removeBubble' }, '*'); }, + openTab(url: string): void { + window.parent.postMessage({ + "type": 'openTab', + "data": { + url + } as OpenTabEvent + }, '*'); + }, + + goToPage(url: string): void { + window.parent.postMessage({ + "type": 'goToPage', + "data": { + url + } as GoToPageEvent + }, '*'); + }, + openCoWebSite(url: string): void { window.parent.postMessage({ "type": 'openCoWebSite', From a3165a0540f8aef8477c62e6b4d4dad6adac1150 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 09:39:04 +0200 Subject: [PATCH 29/87] pause for loading tileset on the fly --- front/src/Phaser/Game/GameScene.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5b049ebc..3df7e093 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -910,12 +910,12 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) - this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { +/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); this.load.image('logo', tileset.imgUrl); this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); - })) + }))*/ } From b18b2fe0e31c5c6481f184b754c581ac2e4cd6a7 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 09:50:59 +0200 Subject: [PATCH 30/87] preparation for merge with metadataScriptApi --- .../{ApiUpdateTileEvent.ts => ChangeTileEvent.ts} | 6 ++---- front/src/Api/IframeListener.ts | 6 +++--- front/src/Phaser/Game/GameScene.ts | 10 +++++----- 3 files changed, 10 insertions(+), 12 deletions(-) rename front/src/Api/Events/{ApiUpdateTileEvent.ts => ChangeTileEvent.ts} (70%) diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ChangeTileEvent.ts similarity index 70% rename from front/src/Api/Events/ApiUpdateTileEvent.ts rename to front/src/Api/Events/ChangeTileEvent.ts index 094596a4..5a9183ca 100644 --- a/front/src/Api/Events/ApiUpdateTileEvent.ts +++ b/front/src/Api/Events/ChangeTileEvent.ts @@ -1,9 +1,7 @@ - import * as tg from "generic-type-guard"; -export const updateTile = "updateTile" -export const isUpdateTileEvent = tg.isArray( +export const isChangeTileEvent = tg.isArray( new tg.IsInterface().withProperties({ x: tg.isNumber, y: tg.isNumber, @@ -14,4 +12,4 @@ export const isUpdateTileEvent = tg.isArray( /** * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. */ -export type UpdateTileEvent = tg.GuardedType; \ No newline at end of file +export type ChangeTileEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index f97e80ae..d59c9140 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -13,7 +13,7 @@ import { scriptUtils } from "./ScriptUtils"; import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { isLoadPageEvent } from './Events/LoadPageEvent'; -import { isUpdateTileEvent, UpdateTileEvent } from './Events/ApiUpdateTileEvent'; +import { isChangeTileEvent, ChangeTileEvent } from './Events/ChangeTileEvent'; /** @@ -58,7 +58,7 @@ class IframeListener { private readonly _removeBubbleStream: Subject = new Subject(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); - private readonly _updateTileEvent: Subject = new Subject(); + private readonly _updateTileEvent: Subject = new Subject(); public readonly updateTileEvent = this._updateTileEvent.asObservable(); private readonly iframes = new Set(); @@ -114,7 +114,7 @@ class IframeListener { this._removeBubbleStream.next(); } else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { this._loadPageStream.next(payload.data.url); - } else if (payload.type == "updateTile" && isUpdateTileEvent(payload.data)) { + } else if (payload.type == "updateTile" && isChangeTileEvent(payload.data)) { this._updateTileEvent.next(payload.data) } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 674087e0..fc5bf80f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -134,7 +134,7 @@ export class GameScene extends ResizableScene implements CenterListener { MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayersByKey: Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; - Layers!: Array; + Layers!: Array; Objects!: Array; mapFile!: ITiledMap; groups: Map; @@ -395,12 +395,12 @@ export class GameScene extends ResizableScene implements CenterListener { this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); //add layer on map - this.Layers = new Array(); + this.Layers = new Array(); let depth = -2; for (const layer of this.gameMap.layersIterator) { if (layer.type === 'tilelayer') { - this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); + this.addLayer(this.Map.createLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { @@ -1105,13 +1105,13 @@ ${escapedMessage} this.cameras.main.setZoom(ZOOM_LEVEL); } - addLayer(Layer: Phaser.Tilemaps.StaticTilemapLayer) { + addLayer(Layer: Phaser.Tilemaps.TilemapLayer) { this.Layers.push(Layer); } createCollisionWithPlayer() { //add collision layer - this.Layers.forEach((Layer: Phaser.Tilemaps.StaticTilemapLayer) => { + this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); From a7b09e91ba95dcd17207179c8b9dd1e6a313028d Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 10:09:58 +0200 Subject: [PATCH 31/87] Revert "Merge branch 'update-game-tiles' into metadataScriptingApi" This reverts commit 428625e61b558004ae37385b21270fdf11864b2a, reversing changes made to a3165a0540f8aef8477c62e6b4d4dad6adac1150. --- front/src/Api/Events/ChangeTileEvent.ts | 15 --------------- front/src/Api/IframeListener.ts | 9 --------- 2 files changed, 24 deletions(-) delete mode 100644 front/src/Api/Events/ChangeTileEvent.ts diff --git a/front/src/Api/Events/ChangeTileEvent.ts b/front/src/Api/Events/ChangeTileEvent.ts deleted file mode 100644 index 5a9183ca..00000000 --- a/front/src/Api/Events/ChangeTileEvent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as tg from "generic-type-guard"; - - -export const isChangeTileEvent = tg.isArray( - new tg.IsInterface().withProperties({ - x: tg.isNumber, - y: tg.isNumber, - tile: tg.isUnion(tg.isNumber, tg.isString), - layer: tg.isUnion(tg.isNumber, tg.isString) - }).get() -); -/** - * A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property. - */ -export type ChangeTileEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index d14a3486..8af0949f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -21,8 +21,6 @@ import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; import type { TagEvent } from "./Events/TagEvent"; import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; -import { isLoadPageEvent } from './Events/LoadPageEvent'; -import { isChangeTileEvent, ChangeTileEvent } from './Events/ChangeTileEvent'; /** @@ -88,9 +86,6 @@ class IframeListener { private readonly _tilesetLoaderStream: Subject = new Subject(); public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable(); - private readonly _updateTileEvent: Subject = new Subject(); - public readonly updateTileEvent = this._updateTileEvent.asObservable(); - private readonly iframes = new Set(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -163,10 +158,6 @@ class IframeListener { this._tagListStream.next(); } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { this._tilesetLoaderStream.next(payload.data); - } else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { - this._loadPageStream.next(payload.data.url); - } else if (payload.type == "updateTile" && isChangeTileEvent(payload.data)) { - this._updateTileEvent.next(payload.data) } } }, false); From 343ad6ea9636838d12e6127f6a8aba59a9d3c324 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 10:11:25 +0200 Subject: [PATCH 32/87] Revert "preparation for merge with metadataScriptApi" This reverts commit b18b2fe0e31c5c6481f184b754c581ac2e4cd6a7. --- front/src/Api/Events/ApiUpdateTileEvent.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 front/src/Api/Events/ApiUpdateTileEvent.ts diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ApiUpdateTileEvent.ts new file mode 100644 index 00000000..e69de29b From 36f0cd1a23206c14b718165875ba4d970def49ed Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 10:11:27 +0200 Subject: [PATCH 33/87] Revert "pause for loading tileset on the fly" This reverts commit a3165a0540f8aef8477c62e6b4d4dad6adac1150. --- front/src/Phaser/Game/GameScene.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 3df7e093..5b049ebc 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -910,12 +910,12 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) -/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { + this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); this.load.image('logo', tileset.imgUrl); this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); - }))*/ + })) } From d4bc999c54a2e7d9cb961c26d847777e9f0e8ad6 Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 10:15:56 +0200 Subject: [PATCH 34/87] pause loading tileset on fly --- front/src/Api/Events/IframeEvent.ts | 2 +- front/src/Api/IframeListener.ts | 10 +++++----- front/src/Phaser/Game/GameScene.ts | 4 ++-- front/src/iframe_api.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 1ee7d1fb..8e4a76f5 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -43,7 +43,7 @@ export type IframeEventMap = { setProperty: SetPropertyEvent getDataLayer: undefined getTag: undefined - tilsetEvent: TilesetEvent + //tilsetEvent: TilesetEvent } export interface IframeEvent { type: T; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 8af0949f..647a95dc 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -20,7 +20,7 @@ import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; import type { TagEvent } from "./Events/TagEvent"; -import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; +//import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; /** @@ -83,8 +83,8 @@ class IframeListener { private readonly _tagListStream: Subject = new Subject(); public readonly tagListStream = this._tagListStream.asObservable(); - private readonly _tilesetLoaderStream: Subject = new Subject(); - public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable(); +/* private readonly _tilesetLoaderStream: Subject = new Subject(); + public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable();*/ private readonly iframes = new Set(); private readonly scripts = new Map(); @@ -156,8 +156,8 @@ class IframeListener { this._registerMenuCommandStream.next(payload.data.menutItem) } else if (payload.type == "getTag") { this._tagListStream.next(); - } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { - this._tilesetLoaderStream.next(payload.data); +/* } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { + this._tilesetLoaderStream.next(payload.data);*/ } } }, false); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5b049ebc..3df7e093 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -910,12 +910,12 @@ ${escapedMessage} iframeListener.sendUserTagList({list: this.connection.getAllTag()}); })) - this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { +/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); this.load.image('logo', tileset.imgUrl); this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); - })) + }))*/ } diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 5a3336a4..b2eac975 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -49,7 +49,7 @@ interface WorkAdventureApi { getNickName(): Promise; getTagUser(): Promise; getMap(): Promise - loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; + //loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void } @@ -165,7 +165,7 @@ window.WA = { }) }, - loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { +/* loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { postToParent({ type: "tilsetEvent", data: { @@ -177,7 +177,7 @@ window.WA = { spacing: spacing } as TilesetEvent }) - }, + },*/ getTagUser(): Promise { return getTag().then((res) => { From 7c44d747de474ea8c476ff77d75158ee7d1339f5 Mon Sep 17 00:00:00 2001 From: GRL78 <80678534+GRL78@users.noreply.github.com> Date: Tue, 25 May 2021 11:02:25 +0200 Subject: [PATCH 35/87] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Négrier --- docs/maps/api-reference.md | 22 ++++++--------- front/src/Api/Events/GameStateEvent.ts | 4 +-- front/src/Api/Events/HasPlayerMovedEvent.ts | 2 +- front/src/Phaser/Game/GameMap.ts | 31 ++------------------- 4 files changed, 14 insertions(+), 45 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 889ed3ac..30d0f1ea 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -258,7 +258,7 @@ WA.hideLayer('bottom'); WA.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; ``` -Set the value of the "propertyName" property of the layer "layerName" at "propertyValue". If the property doesn't exist, create the property "propertyName" and set the value of the property at "propertyValue". +Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. Example : @@ -266,12 +266,12 @@ Example : WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` -### Listen player movement +### Listen to player movement ``` onPlayerMove(callback: HasPlayerMovedEventCallback): void; ``` -Listens to the movement of the current user and calls the callback. Send a event when current user stop moving, change direction and every 200ms when moving in the same direction. +Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction. The event has the following attributes : * **moving (boolean):** **true** when the current player is moving, **false** otherwise. @@ -281,7 +281,7 @@ The event has the following attributes : **callback:** the function that will be called when the current player is moving. It contains the event. -Exemple : +Example : ```javascript WA.onPlayerMove(console.log); ``` @@ -292,7 +292,7 @@ WA.onPlayerMove(console.log); getMap(): Promise ``` -Return a promise of an ITiledMap that contains the JSON file of the map plus the property set by a script. +Returns a promise that resolves to the JSON file of the map. Please note that if you modified the map (for instance by calling `WA.setProperty`, the data returned by `getMap` will contain those changes. Example : ```javascript @@ -360,23 +360,20 @@ WA.getStartLayerName().then((starLayerName) => {console.log(starLayerName)}); ``` registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) ``` -Add a custom menu named "commandDescriptor" in the menu that call the callback when clicked. +Add a custom menu item containing the text `commandDescriptor`. A click on the menu will trigger the `callback`. Example : ```javascript -let chatbotEnabled = false -WA.registerMenuCommand('help', () => { - chatbotEnabled = true; - WA.onChatMessage ... +WA.registerMenuCommand('About', () => { + console.log("The About menu was clicked"); }); -``` ### Getting the list of tags of the current user ``` getTagUser(): Promise ``` -Return the list of all the tags that has the current user. If the current user has no tag, return an empty list. If there is no connection with the room, return nothing. +Returns the tags of the current user. If the current user has no tag, returns an empty list. Example : ```javascript @@ -384,4 +381,3 @@ WA.getTagUser().then((tagList) => { ... }); ``` - diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 72e40898..946febe8 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -23,6 +23,6 @@ export const isGameStateEvent = startLayerName: tg.isUnion(tg.isString, tg.isNull) }).get(); /** - * A message sent from the game to the iFrame when the gameState is got by the script + * A message sent from the game to the iFrame when the gameState is received by the script */ -export type GameStateEvent = tg.GuardedType; \ No newline at end of file +export type GameStateEvent = tg.GuardedType; diff --git a/front/src/Api/Events/HasPlayerMovedEvent.ts b/front/src/Api/Events/HasPlayerMovedEvent.ts index 28603284..e7750367 100644 --- a/front/src/Api/Events/HasPlayerMovedEvent.ts +++ b/front/src/Api/Events/HasPlayerMovedEvent.ts @@ -11,7 +11,7 @@ export const isHasPlayerMovedEvent = }).get(); /** - * A message sent from the game to the iFrame when the player move after the iFrame send a message to the game that it want to listen to the position of the player + * A message sent from the game to the iFrame to notify a movement from the current player. */ export type HasPlayerMovedEvent = tg.GuardedType; diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index d63a67e0..cc109751 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -117,41 +117,14 @@ export class GameMap { } public findLayer(layerName: string): ITiledMapLayer | undefined { - let i = 0; - let found = false; - while (!found && i layer.name = layerName); } public findPhaserLayer(layerName: string): TilemapLayer | undefined { - let i = 0; - let found = false; - while (!found && i layer.layer.name = layerName); } public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { - console.log('Add'); for (const phaserLayer of this.phaserLayers) { phaserLayer.tileset.push(terrain); } From a5cb93541afad9a95fe4614bd96a8f7d8ee99a9b Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 25 May 2021 17:21:02 +0200 Subject: [PATCH 36/87] correction from code review --- front/src/Api/Events/ApiUpdateTileEvent.ts | 0 front/src/Api/Events/GameStateEvent.ts | 25 ++--- front/src/Api/Events/HasPlayerMovedEvent.ts | 2 +- front/src/Api/Events/IframeEvent.ts | 5 - front/src/Api/Events/TagEvent.ts | 10 -- front/src/Api/Events/TilesetEvent.ts | 15 --- front/src/Api/IframeListener.ts | 15 +-- front/src/Connexion/RoomConnection.ts | 9 +- front/src/Phaser/Game/GameScene.ts | 27 ++--- front/src/iframe_api.ts | 111 +++++++------------- 10 files changed, 58 insertions(+), 161 deletions(-) delete mode 100644 front/src/Api/Events/ApiUpdateTileEvent.ts delete mode 100644 front/src/Api/Events/TagEvent.ts delete mode 100644 front/src/Api/Events/TilesetEvent.ts diff --git a/front/src/Api/Events/ApiUpdateTileEvent.ts b/front/src/Api/Events/ApiUpdateTileEvent.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 72e40898..704cd962 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -1,26 +1,13 @@ import * as tg from "generic-type-guard"; -/*export const isPositionState = new tg.IsInterface().withProperties({ - x: tg.isNumber, - y: tg.isNumber -}).get() -export const isPlayerState = new tg.IsInterface() - .withStringIndexSignature( - new tg.IsInterface().withProperties({ - position: isPositionState, - pusherId: tg.isUnion(tg.isNumber, tg.isUndefined) - }).get() - ).get() - -export type PlayerStateObject = tg.GuardedType;*/ - export const isGameStateEvent = new tg.IsInterface().withProperties({ - roomId: tg.isString, - mapUrl: tg.isString, - nickname: tg.isUnion(tg.isString, tg.isNull), - uuid: tg.isUnion(tg.isString, tg.isUndefined), - startLayerName: tg.isUnion(tg.isString, tg.isNull) + roomId: tg.isString, + mapUrl: tg.isString, + nickname: tg.isUnion(tg.isString, tg.isNull), + uuid: tg.isUnion(tg.isString, tg.isUndefined), + startLayerName: tg.isUnion(tg.isString, tg.isNull), + tags : tg.isArray(tg.isString), }).get(); /** * A message sent from the game to the iFrame when the gameState is got by the script diff --git a/front/src/Api/Events/HasPlayerMovedEvent.ts b/front/src/Api/Events/HasPlayerMovedEvent.ts index 28603284..5fe2a1e2 100644 --- a/front/src/Api/Events/HasPlayerMovedEvent.ts +++ b/front/src/Api/Events/HasPlayerMovedEvent.ts @@ -4,7 +4,7 @@ import * as tg from "generic-type-guard"; export const isHasPlayerMovedEvent = new tg.IsInterface().withProperties({ - direction: tg.isString, + direction: tg.isElementOf('right', 'left', 'up', 'down'), moving: tg.isBoolean, x: tg.isNumber, y: tg.isNumber diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 8e4a76f5..1bab019a 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,8 +15,6 @@ import type { UserInputChatEvent } from './UserInputChatEvent'; import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; -import type { TagEvent } from "./TagEvent"; -import type { TilesetEvent } from "./TilesetEvent"; export interface TypedMessageEvent extends MessageEvent { data: T @@ -24,7 +22,6 @@ export interface TypedMessageEvent extends MessageEvent { export type IframeEventMap = { getState: GameStateEvent, - // updateTile: UpdateTileEvent registerMenuCommand: MenuItemRegisterEvent chat: ChatEvent, openPopup: OpenPopupEvent @@ -42,7 +39,6 @@ export type IframeEventMap = { hideLayer: LayerEvent setProperty: SetPropertyEvent getDataLayer: undefined - getTag: undefined //tilsetEvent: TilesetEvent } export interface IframeEvent { @@ -63,7 +59,6 @@ export interface IframeResponseEventMap { hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent - tagList: TagEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/TagEvent.ts b/front/src/Api/Events/TagEvent.ts deleted file mode 100644 index 66665403..00000000 --- a/front/src/Api/Events/TagEvent.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as tg from "generic-type-guard"; - -export const isTagEvent = - new tg.IsInterface().withProperties({ - list: tg.isArray(tg.isString), - }).get(); -/** - * A message sent from the iFrame to the game to show/hide a layer. - */ -export type TagEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/Events/TilesetEvent.ts b/front/src/Api/Events/TilesetEvent.ts deleted file mode 100644 index eab33bf7..00000000 --- a/front/src/Api/Events/TilesetEvent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as tg from "generic-type-guard"; - -export const isTilesetEvent = - new tg.IsInterface().withProperties({ - name : tg.isString, - imgUrl : tg.isString, - tilewidth : tg.isNumber, - tileheight : tg.isNumber, - margin : tg.isNumber, - spacing : tg.isNumber, - }).get(); -/** - * A message sent from the iFrame to the game to show/hide a layer. - */ -export type TilesetEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 647a95dc..ec340b16 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -19,7 +19,6 @@ import { Math } from 'phaser'; import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; -import type { TagEvent } from "./Events/TagEvent"; //import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; @@ -80,9 +79,6 @@ class IframeListener { private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); - private readonly _tagListStream: Subject = new Subject(); - public readonly tagListStream = this._tagListStream.asObservable(); - /* private readonly _tilesetLoaderStream: Subject = new Subject(); public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable();*/ @@ -154,9 +150,7 @@ class IframeListener { this._dataLayerChangeStream.next(); } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { this._registerMenuCommandStream.next(payload.data.menutItem) - } else if (payload.type == "getTag") { - this._tagListStream.next(); -/* } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { +/* } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { this._tilesetLoaderStream.next(payload.data);*/ } } @@ -164,13 +158,6 @@ class IframeListener { } - sendUserTagList(tagList: TagEvent){ - this.postMessage({ - 'type' : 'tagList', - 'data' : tagList - }) - } - sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { this.postMessage({ 'type' : 'dataLayer', diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 1cb4a97d..8bfa3b6a 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -169,9 +169,9 @@ export class RoomConnection implements RoomConnection { } else if (message.hasWorldfullmessage()) { worldFullMessageStream.onMessage(); this.closed = true; - // // } else if (message.hasWorldconnexionmessage()) { - // worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); - // this.closed = true; + } else if (message.hasWorldconnexionmessage()) { + worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); + this.closed = true; } else if (message.hasWebrtcsignaltoclientmessage()) { this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { @@ -600,9 +600,6 @@ export class RoomConnection implements RoomConnection { } public getAllTag() : string[] { - this.tags.push('TEST'); - this.tags.push('TEST 2'); - this.tags.push('TEST 3'); return this.tags; } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 3df7e093..5e540770 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -864,15 +864,6 @@ ${escapedMessage} this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(() => { this.userInputManager.restoreControls(); })); - this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { - iframeListener.sendFrozenGameStateEvent({ - mapUrl: this.MapUrlFile, - startLayerName: this.startLayerName, - uuid: localUserStore.getLocalUser()?.uuid, - nickname: localUserStore.getName(), - roomId: this.RoomId, - }) - })); let scriptedBubbleSprite: Sprite; this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(() => { @@ -886,12 +877,10 @@ ${escapedMessage} })); this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent)=>{ - console.log('show'); this.setLayerVisibility(layerEvent.name, true); })); this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{ - console.log('hide'); this.setLayerVisibility(layerEvent.name, false); })); @@ -903,12 +892,16 @@ ${escapedMessage} iframeListener.sendDataLayerEvent({data: this.gameMap.getMap()}); })) - this.iframeSubscriptionList.push(iframeListener.tagListStream.subscribe(()=> { - if (this.connection === undefined) { - return; - } - iframeListener.sendUserTagList({list: this.connection.getAllTag()}); - })) + this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { + iframeListener.sendFrozenGameStateEvent({ + mapUrl: this.MapUrlFile, + startLayerName: this.startLayerName, + uuid: localUserStore.getLocalUser()?.uuid, + nickname: localUserStore.getName(), + roomId: this.RoomId, + tags: this.connection ? this.connection.getAllTag() : [] + }) + })); /* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { //this.load.tilemapTiledJSON('logo', tileset.imgUrl); diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index b2eac975..f62b77a4 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -17,8 +17,6 @@ import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; import type { ITiledMap } from "./Phaser/Map/ITiledMap"; import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; -import {TagEvent, isTagEvent} from "./Api/Events/TagEvent"; -import type { TilesetEvent } from "./Api/Events/TilesetEvent"; interface WorkAdventureApi { sendChatMessage(message: string, author: string): void; @@ -42,18 +40,26 @@ interface WorkAdventureApi { displayBubble(): void; removeBubble(): void; registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void - getMapUrl(): Promise; - getUuid(): Promise; - getRoomId(): Promise; - getStartLayerName(): Promise; - getNickName(): Promise; - getTagUser(): Promise; - getMap(): Promise + getCurrentUser(): Promise + getCurrentRoom(): Promise //loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void } +interface User { + id: string | undefined + nickName: string | null + tags: string[] +} + +interface Room { + id: string + mapUrl: string + map: ITiledMap + startLayer: string | null +} + declare global { // eslint-disable-next-line no-var var WA: WorkAdventureApi @@ -101,12 +107,14 @@ class Popup { }, '*'); } } -function uuidv4() { + +/*function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); -} +}*/ + function getGameState(): Promise { if (immutableData) { return Promise.resolve(immutableData); @@ -131,34 +139,21 @@ function getDataLayer(): Promise { }) } -function getTag(): Promise { - return new Promise((resolver, thrower) => { - tagResolver.push((resolver)); - postToParent({ - type: "getTag", - data: undefined - }) - }) -} - const gameStateResolver: Array<(event: GameStateEvent) => void> = [] const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] -const tagResolver: Array<(event : TagEvent) => void> = [] let immutableData: GameStateEvent; -const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} - +//const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} +const callbackPlayerMoved: Array<(event: HasPlayerMovedEvent) => void> = [] function postToParent(content: IframeEvent) { window.parent.postMessage(content, "*") } -let playerUuid: string | undefined; window.WA = { onPlayerMove(callback: HasPlayerMovedEventCallback): void { - playerUuid = uuidv4(); - callbackPlayerMoved[playerUuid] = callback; + callbackPlayerMoved.push(callback); postToParent({ type: "onPlayerMove", data: undefined @@ -179,45 +174,17 @@ window.WA = { }) },*/ - getTagUser(): Promise { - return getTag().then((res) => { - return res.list; + getCurrentUser(): Promise { + return getGameState().then((gameState) => { + return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags}; }) }, - getMap(): Promise { - return getDataLayer().then((res) => { - return res.data as ITiledMap; - }) - }, - - getNickName(): Promise { - return getGameState().then((res) => { - return res.nickname; - }) - }, - - getMapUrl(): Promise { - return getGameState().then((res) => { - return res.mapUrl; - }) - }, - - getUuid(): Promise { - return getGameState().then((res) => { - return res.uuid; - }) - }, - - getRoomId(): Promise { - return getGameState().then((res) => { - return res.roomId; - }) - }, - - getStartLayerName(): Promise { - return getGameState().then((res) => { - return res.startLayerName; + getCurrentRoom(): Promise { + return getGameState().then((gameState) => { + return getDataLayer().then((mapJson) => { + return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName}; + }) }) }, @@ -411,22 +378,18 @@ window.addEventListener('message', message => { resolver(payloadData); }) immutableData = payloadData; - } else if (payload.type == "hasPlayerMoved" && isHasPlayerMovedEvent(payloadData) && playerUuid) { - callbackPlayerMoved[playerUuid](payloadData) + } else if (payload.type == "hasPlayerMoved" && isHasPlayerMovedEvent(payloadData)) { + callbackPlayerMoved.forEach(callback => { + callback(payloadData); + }) } else if (payload.type == "dataLayer" && isDataLayerEvent(payloadData)) { dataLayerResolver.forEach(resolver => { resolver(payloadData); }) - } else if (payload.type == "menuItemClicked" && isMenuItemClickedEvent(payload.data)) { - const callback = menuCallbacks.get(payload.data.menuItem); + } else if (payload.type == "menuItemClicked" && isMenuItemClickedEvent(payloadData)) { + const callback = menuCallbacks.get(payloadData.menuItem); if (callback) { - callback(payload.data.menuItem) - } - } else { - if (payload.type == "tagList" && isTagEvent(payloadData)) { - tagResolver.forEach(resolver => { - resolver(payloadData); - }) + callback(payloadData.menuItem) } } } From c8e2416e081a5450b24b3498b384038ebb82cd6d Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 26 May 2021 10:41:33 +0200 Subject: [PATCH 37/87] documentation of getCurrentUser, getCurrentRoom and on working with group layer --- docs/maps/api-reference.md | 111 +++++++++++++------------------------ front/src/iframe_api.ts | 4 ++ 2 files changed, 44 insertions(+), 71 deletions(-) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 30d0f1ea..6a4dd7ab 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -236,8 +236,7 @@ mySound.play(config); mySound.stop(); ``` -### Show / Hide a layer - +### Show / Hide a layer ``` WA.showLayer(layerName : string): void WA.hideLayer(layerName : string) : void @@ -245,7 +244,6 @@ WA.hideLayer(layerName : string) : void These 2 methods can be used to show and hide a layer. Example : - ```javascript WA.showLayer('bottom'); //... @@ -260,8 +258,7 @@ WA.setProperty(layerName : string, propertyName : string, propertyValue : string Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. -Example : - +Example : ```javascript WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` @@ -286,74 +283,42 @@ Example : WA.onPlayerMove(console.log); ``` -### Getting the map - +### Getting informations on the current user ``` -getMap(): Promise +getCurrentUser(): Promise ``` - -Returns a promise that resolves to the JSON file of the map. Please note that if you modified the map (for instance by calling `WA.setProperty`, the data returned by `getMap` will contain those changes. +Return a promise that resolves to a `User` object with the following attributes : +* **id (string) :** ID of the current user +* **nickName (string) :** name displayed above the current user +* **tags (string[]) :** list of all the tags of the current user Example : ```javascript -WA.getMap().then((data) => console.log(data.layers)); +WA.getCurrentUser().then((user) => { + if (user.nickName === 'ABC') { + console.log(user.tags); + } +}) ``` -### Getting the url of the JSON file map - +### Getting informations on the current room ``` -getMapUrl(): Promise +getCurrentRoom(): Promise ``` - -Return a promise of the url of the JSON file map. +Return a promise that resolves to a `Room` object with the following attributes : +* **id (string) :** ID of the current room +* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. +* **mapUrl (string) :** Url of the JSON map file +* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer Example : ```javascript -WA.getMapUrl().then((mapUrl) => {console.log(mapUrl)}); -``` - -### Getting the roomID -``` -getRoomId(): Promise -``` -Return a promise of the ID of the current room. - -Example : -```javascript -WA.getRoomId().then((roomId) => console.log(roomId)); -``` - -### Getting the UUID of the current user -``` -getUuid(): Promise -``` -Return a promise of the ID of the current user. - -Example : -```javascript -WA.getUuid().then((uuid) => {console.log(uuid)}); -``` - -### Getting the nickname of the current user -``` -getNickName(): Promise -``` -Return a promise of the nickname of the current user. - -Example : -```javascript -WA.getNickName().then((nickname) => {console.log(nickname)}); -``` - -### Getting the name of the layer where the current user started (if other than start) -``` -getStartLayerName(): Promise -``` -Return a promise of the name of the layer where the current user started if the name is different than "start". - -Example : -```javascript -WA.getStartLayerName().then((starLayerName) => {console.log(starLayerName)}); +WA.getCurrentRoom((room) => { + if (room.id === '42') { + console.log(room.map); + window.open(room.mapUrl, '_blank'); + } +}) ``` ### Add a custom menu @@ -367,17 +332,21 @@ Example : WA.registerMenuCommand('About', () => { console.log("The About menu was clicked"); }); - -### Getting the list of tags of the current user -``` -getTagUser(): Promise ``` -Returns the tags of the current user. If the current user has no tag, returns an empty list. + +### Working with group layers +If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together. Example : -```javascript -WA.getTagUser().then((tagList) => { - ... -}); -``` +
+
+ +
+
+ +The name of the layers of this map are : +* `entries/start` +* `bottom/ground/under` +* `bottom/build/carpet` +* `wall` \ No newline at end of file diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index f62b77a4..8da1fa23 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -201,6 +201,7 @@ window.WA = { } as ChatEvent }, '*'); }, + showLayer(layer: string) : void { window.parent.postMessage({ 'type' : 'showLayer', @@ -209,6 +210,7 @@ window.WA = { } as LayerEvent }, '*'); }, + hideLayer(layer: string) : void { window.parent.postMessage({ 'type' : 'hideLayer', @@ -217,6 +219,7 @@ window.WA = { } as LayerEvent }, '*'); }, + setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { window.parent.postMessage({ 'type' : 'setProperty', @@ -227,6 +230,7 @@ window.WA = { } as SetPropertyEvent }, '*'); }, + disablePlayerControls(): void { window.parent.postMessage({ 'type': 'disablePlayerControls' }, '*'); }, From e1f0192e617b8118474abfcf7382f1cefb6bd649 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 26 May 2021 17:18:38 +0200 Subject: [PATCH 38/87] Adding and updating test map for metadata --- maps/tests/Metadata/ScriptMap.json | 219 --------------- maps/tests/Metadata/TagList.html | 19 -- maps/tests/Metadata/TagList.json | 254 ------------------ maps/tests/Metadata/getCurrentRoom.html | 16 ++ ...{getGameState.json => getCurrentRoom.json} | 46 ++-- maps/tests/Metadata/getCurrentUser.html | 15 ++ ...getGameState2.json => getCurrentUser.json} | 43 ++- maps/tests/Metadata/getGameState.html | 42 --- maps/tests/Metadata/getGameState2.html | 40 --- maps/tests/Metadata/script.js | 1 - 10 files changed, 87 insertions(+), 608 deletions(-) delete mode 100644 maps/tests/Metadata/ScriptMap.json delete mode 100644 maps/tests/Metadata/TagList.html delete mode 100644 maps/tests/Metadata/TagList.json create mode 100644 maps/tests/Metadata/getCurrentRoom.html rename maps/tests/Metadata/{getGameState.json => getCurrentRoom.json} (90%) create mode 100644 maps/tests/Metadata/getCurrentUser.html rename maps/tests/Metadata/{getGameState2.json => getCurrentUser.json} (86%) delete mode 100644 maps/tests/Metadata/getGameState.html delete mode 100644 maps/tests/Metadata/getGameState2.html delete mode 100644 maps/tests/Metadata/script.js diff --git a/maps/tests/Metadata/ScriptMap.json b/maps/tests/Metadata/ScriptMap.json deleted file mode 100644 index 93972a73..00000000 --- a/maps/tests/Metadata/ScriptMap.json +++ /dev/null @@ -1,219 +0,0 @@ -{ "compressionlevel":-1, - "height":10, - "infinite":false, - "layers":[ - { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":1, - "name":"start", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], - "height":10, - "id":2, - "name":"bottom", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "draworder":"topdown", - "id":5, - "name":"floorLayer", - "objects":[], - "opacity":1, - "type":"objectgroup", - "visible":true, - "x":0, - "y":0 - }], - "nextlayerid":10, - "nextobjectid":2, - "orientation":"orthogonal", - "properties":[ - { - "name":"script", - "type":"string", - "value":"script.js" - }], - "renderorder":"right-down", - "tiledversion":"1.4.3", - "tileheight":32, - "tilesets":[ - { - "columns":8, - "firstgid":1, - "image":"tileset_dungeon.png", - "imageheight":256, - "imagewidth":256, - "margin":0, - "name":"TDungeon", - "spacing":0, - "tilecount":64, - "tileheight":32, - "tiles":[ - { - "id":0, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":1, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":2, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":3, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":4, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":8, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":9, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":10, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":11, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":12, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":16, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":17, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":18, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":19, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":20, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }], - "tilewidth":32 - }, - { - "columns":8, - "firstgid":65, - "image":"floortileset.png", - "imageheight":288, - "imagewidth":256, - "margin":0, - "name":"Floor", - "spacing":0, - "tilecount":72, - "tileheight":32, - "tilewidth":32 - }], - "tilewidth":32, - "type":"map", - "version":1.4, - "width":10 -} \ No newline at end of file diff --git a/maps/tests/Metadata/TagList.html b/maps/tests/Metadata/TagList.html deleted file mode 100644 index 73bdc368..00000000 --- a/maps/tests/Metadata/TagList.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - -
- - \ No newline at end of file diff --git a/maps/tests/Metadata/TagList.json b/maps/tests/Metadata/TagList.json deleted file mode 100644 index cced49a3..00000000 --- a/maps/tests/Metadata/TagList.json +++ /dev/null @@ -1,254 +0,0 @@ -{ "compressionlevel":-1, - "height":10, - "infinite":false, - "layers":[ - { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":1, - "name":"start", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], - "height":10, - "id":2, - "name":"bottom", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":4, - "name":"metadata", - "opacity":1, - "properties":[ - { - "name":"openWebsite", - "type":"string", - "value":"TagList.html" - }, - { - "name":"openWebsiteAllowApi", - "type":"bool", - "value":true - }], - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "draworder":"topdown", - "id":5, - "name":"floorLayer", - "objects":[ - { - "height":131.903791109293, - "id":1, - "name":"", - "rotation":0, - "text": - { - "fontfamily":"Sans Serif", - "pixelsize":9, - "text":"Test : \nWalk on the grass, an iframe open, click on the 'Get Tag List' button.\nResult : \nThe list of the tag is displayed in the iframe.\n\n\n", - "wrap":true - }, - "type":"", - "visible":true, - "width":305.097705765524, - "x":14.750638909983, - "y":188.268561247737 - }], - "opacity":1, - "type":"objectgroup", - "visible":true, - "x":0, - "y":0 - }], - "nextlayerid":10, - "nextobjectid":2, - "orientation":"orthogonal", - "renderorder":"right-down", - "tiledversion":"1.4.3", - "tileheight":32, - "tilesets":[ - { - "columns":8, - "firstgid":1, - "image":"tileset_dungeon.png", - "imageheight":256, - "imagewidth":256, - "margin":0, - "name":"TDungeon", - "spacing":0, - "tilecount":64, - "tileheight":32, - "tiles":[ - { - "id":0, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":1, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":2, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":3, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":4, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":8, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":9, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":10, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":11, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":12, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":16, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":17, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":18, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":19, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":20, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }], - "tilewidth":32 - }, - { - "columns":8, - "firstgid":65, - "image":"floortileset.png", - "imageheight":288, - "imagewidth":256, - "margin":0, - "name":"Floor", - "spacing":0, - "tilecount":72, - "tileheight":32, - "tilewidth":32 - }], - "tilewidth":32, - "type":"map", - "version":1.4, - "width":10 -} \ No newline at end of file diff --git a/maps/tests/Metadata/getCurrentRoom.html b/maps/tests/Metadata/getCurrentRoom.html new file mode 100644 index 00000000..b290c6a4 --- /dev/null +++ b/maps/tests/Metadata/getCurrentRoom.html @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState.json b/maps/tests/Metadata/getCurrentRoom.json similarity index 90% rename from maps/tests/Metadata/getGameState.json rename to maps/tests/Metadata/getCurrentRoom.json index a005ee8a..c14bb946 100644 --- a/maps/tests/Metadata/getGameState.json +++ b/maps/tests/Metadata/getCurrentRoom.json @@ -9,6 +9,24 @@ "height":10, "infinite":false, "layers":[ + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":10, + "name":"HereYouAppered", + "opacity":1, + "properties":[ + { + "name":"startLayer", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":10, @@ -43,7 +61,7 @@ { "name":"openWebsite", "type":"string", - "value":"getGameState.html" + "value":"getCurrentRoom.html" }, { "name":"openWebsiteAllowApi", @@ -56,31 +74,13 @@ "x":0, "y":0 }, - { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":8, - "name":"exit", - "opacity":1, - "properties":[ - { - "name":"exitUrl", - "type":"string", - "value":"getGameState2.json#HereYouAppear" - }], - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, { "draworder":"topdown", "id":5, "name":"floorLayer", "objects":[ { - "height":218.263975699515, + "height":191.607568521364, "id":1, "name":"", "rotation":0, @@ -88,14 +88,14 @@ { "fontfamily":"Sans Serif", "pixelsize":9, - "text":"Start the test : \nWalk on the grass, an iframe open.\n\nTest : \nClick on the 'nickname' button.\nResult : \nYour nickname appears.\n\nTest : \nClick on the 'roomID' button.\nResult : \nAn ID appears.\n\nTest : \nClick on the 'UUID' button.\nResult : \nAn ID appears.\n\nFinally : \nWalk on the red tiles to continue the testing.\n\n", + "text":"Test : \nWalk on the grass and open the console.\n\nResult : \nYou should see a console.log() of the following attributes : \n\t- id : ID of the current room\n\t- map : data of the JSON file of the map\n\t- mapUrl : url of the JSON file of the map\n\t- startLayer : Name of the layer where the current user started (HereYouAppered)\n\n\n", "wrap":true }, "type":"", "visible":true, "width":305.097705765524, "x":14.750638909983, - "y":101.908376657515 + "y":128.564783835666 }], "opacity":1, "type":"objectgroup", @@ -103,7 +103,7 @@ "x":0, "y":0 }], - "nextlayerid":9, + "nextlayerid":11, "nextobjectid":2, "orientation":"orthogonal", "renderorder":"right-down", diff --git a/maps/tests/Metadata/getCurrentUser.html b/maps/tests/Metadata/getCurrentUser.html new file mode 100644 index 00000000..318fdf1b --- /dev/null +++ b/maps/tests/Metadata/getCurrentUser.html @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState2.json b/maps/tests/Metadata/getCurrentUser.json similarity index 86% rename from maps/tests/Metadata/getGameState2.json rename to maps/tests/Metadata/getCurrentUser.json index 04127918..9efd0d09 100644 --- a/maps/tests/Metadata/getGameState2.json +++ b/maps/tests/Metadata/getCurrentUser.json @@ -22,10 +22,10 @@ "y":0 }, { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], "height":10, - "id":9, - "name":"HereYouAppear", + "id":2, + "name":"bottom", "opacity":1, "type":"tilelayer", "visible":true, @@ -34,11 +34,17 @@ "y":0 }, { - "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":10, - "id":2, - "name":"bottom", + "id":9, + "name":"exit", "opacity":1, + "properties":[ + { + "name":"exitUrl", + "type":"string", + "value":"getCurrentRoom.json#HereYouAppered" + }], "type":"tilelayer", "visible":true, "width":10, @@ -55,7 +61,7 @@ { "name":"openWebsite", "type":"string", - "value":"getGameState2.html" + "value":"getCurrentUser.html" }, { "name":"openWebsiteAllowApi", @@ -74,7 +80,7 @@ "name":"floorLayer", "objects":[ { - "height":200.31900227817, + "height":151.839293303871, "id":1, "name":"", "rotation":0, @@ -82,14 +88,14 @@ { "fontfamily":"Sans Serif", "pixelsize":9, - "text":"Start the test : \nWalk on the grass, an iframe open.\n\nTest : \nClick on the 'startLayer' button.\nResult : \nThe name of the layer where you start appears. (only work when the start layer is not 'start')\n\nTest : \nClick on the 'mapUrl' button.\nResult : \nThe url of the JSON file of the map is displayed in the console.log().\n\nTest : \nClick on the 'Map' button.\nResult : \nThe JSON file map appears.\n\n\n", + "text":"Test : \nWalk on the grass, open the console.\n\nResut : \nYou should see a console.log() of the following attributes :\n\t- id : ID of the current user\n\t- nickName : Name of the current user\n\t- tags : List of tags of the current user\n\nFinally : \nWalk on the red tile and continue the test in an another room.", "wrap":true }, "type":"", "visible":true, "width":305.097705765524, "x":14.750638909983, - "y":119.85335007886 + "y":159.621625296353 }], "opacity":1, "type":"objectgroup", @@ -264,6 +270,23 @@ "spacing":0, "tilecount":72, "tileheight":32, + "tiles":[ + { + "animation":[ + { + "duration":100, + "tileid":9 + }, + { + "duration":100, + "tileid":64 + }, + { + "duration":100, + "tileid":55 + }], + "id":0 + }], "tilewidth":32 }], "tilewidth":32, diff --git a/maps/tests/Metadata/getGameState.html b/maps/tests/Metadata/getGameState.html deleted file mode 100644 index f11dab17..00000000 --- a/maps/tests/Metadata/getGameState.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - -
- - -
- - -
- - - - \ No newline at end of file diff --git a/maps/tests/Metadata/getGameState2.html b/maps/tests/Metadata/getGameState2.html deleted file mode 100644 index e8529617..00000000 --- a/maps/tests/Metadata/getGameState2.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - -
- - -
- - -
- - - - \ No newline at end of file diff --git a/maps/tests/Metadata/script.js b/maps/tests/Metadata/script.js deleted file mode 100644 index d04d7952..00000000 --- a/maps/tests/Metadata/script.js +++ /dev/null @@ -1 +0,0 @@ -console.log('script chargé !!!!!'); \ No newline at end of file From 5d8d729bd73711977e3b6a562e34b654961f4893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 27 May 2021 18:25:27 +0200 Subject: [PATCH 39/87] Uncommenting action --- front/src/Connexion/RoomConnection.ts | 8 ++++---- front/src/Phaser/Game/GameScene.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 58c62a78..159db5a2 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -177,9 +177,9 @@ export class RoomConnection implements RoomConnection { } else if (message.hasWorldfullmessage()) { worldFullMessageStream.onMessage(); this.closed = true; - } else if (message.hasWorldconnexionmessage()) { - worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); - this.closed = true; + } else if (message.hasWorldconnexionmessage()) { + worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); + this.closed = true; } else if (message.hasWebrtcsignaltoclientmessage()) { this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { @@ -617,7 +617,7 @@ export class RoomConnection implements RoomConnection { this.socket.send(clientToServerMessage.serializeBinary().buffer); } - public getAllTag() : string[] { + public getAllTags() : string[] { return this.tags; } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 1e4c55f5..a785b7f6 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -923,7 +923,7 @@ ${escapedMessage} uuid: localUserStore.getLocalUser()?.uuid, nickname: localUserStore.getName(), roomId: this.RoomId, - tags: this.connection ? this.connection.getAllTag() : [] + tags: this.connection ? this.connection.getAllTags() : [] }) })); From 858a513569026d4566857919731dbc21d44d221d Mon Sep 17 00:00:00 2001 From: GRL Date: Fri, 28 May 2021 12:13:10 +0200 Subject: [PATCH 40/87] correction of adding custom menu correction of setProperty updating CHANGELOG updating api-reference --- CHANGELOG.md | 7 +++++ docs/maps/api-reference.md | 2 +- front/src/Api/Events/IframeEvent.ts | 5 ++- front/src/Api/IframeListener.ts | 20 ++++++++---- front/src/Phaser/Game/GameMap.ts | 8 ++--- front/src/Phaser/Game/GameScene.ts | 7 ----- front/src/Phaser/Menu/MenuScene.ts | 7 +++++ front/src/iframe_api.ts | 25 +++------------ maps/tests/index.html | 48 +++++++++++++++++++++++++++++ 9 files changed, 87 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dec14540..68a7016f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,13 @@ - Improved virtual joystick size (adapts to the zoom level) - New scripting API features: - Use `WA.loadSound(): Sound` to load / play / stop a sound + - Use `WA.showLayer(): void` to show a layer + - Use `WA.hideLayer(): void` to hide a layer + - Use `WA.setProperty() : void` to add or change existing property of a layer + - Use `WA.onPlayerMove(): void` to track the movement of the current player + - Use `WA.getCurrentUser(): Promise` to get the ID, name and tags of the current player + - Use `WA.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started + - Use `WA.registerMenuCommand(): void` to add a custom menu ### Bug Fixes diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 6a4dd7ab..d4316772 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -323,7 +323,7 @@ WA.getCurrentRoom((room) => { ### Add a custom menu ``` -registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) +registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void ``` Add a custom menu item containing the text `commandDescriptor`. A click on the menu will trigger the `callback`. diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index bb15528d..e5b1c30b 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,8 +15,8 @@ import type { UserInputChatEvent } from './UserInputChatEvent'; import type { DataLayerEvent } from "./DataLayerEvent"; import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; -import type {LoadSoundEvent} from "./LoadSoundEvent"; -import type {PlaySoundEvent} from "./PlaySoundEvent"; +import type { LoadSoundEvent } from "./LoadSoundEvent"; +import type { PlaySoundEvent } from "./PlaySoundEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -42,7 +42,6 @@ export type IframeEventMap = { hideLayer: LayerEvent setProperty: SetPropertyEvent getDataLayer: undefined - //tilsetEvent: TilesetEvent loadSound: LoadSoundEvent playSound: PlaySoundEvent stopSound: null diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index ceeea1c4..d05b416f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -20,7 +20,6 @@ import { Math } from 'phaser'; import type { DataLayerEvent } from "./Events/DataLayerEvent"; import { isMenuItemRegisterEvent } from './Events/MenuItemRegisterEvent'; import type { MenuItemClickedEvent } from './Events/MenuItemClickedEvent'; -//import { isTilesetEvent, TilesetEvent } from "./Events/TilesetEvent"; import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent"; import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; @@ -81,8 +80,8 @@ class IframeListener { private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); -/* private readonly _tilesetLoaderStream: Subject = new Subject(); - public readonly tilesetLoaderStream = this._tilesetLoaderStream.asObservable();*/ + private readonly _unregisterMenuCommandStream: Subject = new Subject(); + public readonly unregisterMenuCommandStream = this._unregisterMenuCommandStream.asObservable(); private readonly _playSoundStream: Subject = new Subject(); public readonly playSoundStream = this._playSoundStream.asObservable(); @@ -94,6 +93,7 @@ class IframeListener { public readonly loadSoundStream = this._loadSoundStream.asObservable(); private readonly iframes = new Set(); + private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); private sendPlayerMove: boolean = false; @@ -103,7 +103,8 @@ class IframeListener { // Let's only accept messages from the iframe that are allowed. // Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). let foundSrc: string | null = null; - for (const iframe of this.iframes) { + let iframe: HTMLIFrameElement; + for (iframe of this.iframes) { if (iframe.contentWindow === message.source) { foundSrc = iframe.src; break; @@ -171,9 +172,12 @@ class IframeListener { } else if (payload.type == "getDataLayer") { this._dataLayerChangeStream.next(); } else if (payload.type == "registerMenuCommand" && isMenuItemRegisterEvent(payload.data)) { + const data = payload.data.menutItem; + // @ts-ignore + this.iframeCloseCallbacks.get(iframe).push(() => { + this._unregisterMenuCommandStream.next(data); + }) this._registerMenuCommandStream.next(payload.data.menutItem) -/* } else if (payload.type == "tilsetEvent" && isTilesetEvent(payload.data)) { - this._tilesetLoaderStream.next(payload.data);*/ } } }, false); @@ -200,9 +204,13 @@ class IframeListener { */ registerIframe(iframe: HTMLIFrameElement): void { this.iframes.add(iframe); + this.iframeCloseCallbacks.set(iframe, []); } unregisterIframe(iframe: HTMLIFrameElement): void { + this.iframeCloseCallbacks.get(iframe)?.forEach(callback => { + callback(); + }); this.iframes.delete(iframe); } diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 34f55d0b..873b6062 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,7 +1,7 @@ -import type {ITiledMap, ITiledMapLayer, ITiledMapTileLayer} from "../Map/ITiledMap"; +import type { ITiledMap, ITiledMapLayer } from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import TilemapLayer = Phaser.Tilemaps.TilemapLayer; -import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes"; +import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -118,11 +118,11 @@ export class GameMap { } public findLayer(layerName: string): ITiledMapLayer | undefined { - return this.flatLayers.find((layer) => layer.name = layerName); + return this.flatLayers.find((layer) => layer.name === layerName); } public findPhaserLayer(layerName: string): TilemapLayer | undefined { - return this.phaserLayers.find((layer) => layer.layer.name = layerName); + return this.phaserLayers.find((layer) => layer.layer.name === layerName); } public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 1e4c55f5..cb2ec0a0 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -927,13 +927,6 @@ ${escapedMessage} }) })); -/* this.iframeSubscriptionList.push(iframeListener.tilesetLoaderStream.subscribe((tileset) => { - //this.load.tilemapTiledJSON('logo', tileset.imgUrl); - this.load.image('logo', tileset.imgUrl); - this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.imgUrl, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing)); - this.gameMap.addTerrain(this.Terrains[this.Terrains.length - 1]); - }))*/ - } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 8957bbce..8a01c259 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -49,7 +49,10 @@ export class MenuScene extends Phaser.Scene { this.subscriptions.add(iframeListener.registerMenuCommandStream.subscribe(menuCommand => { this.addMenuOption(menuCommand); + })) + this.subscriptions.add(iframeListener.unregisterMenuCommandStream.subscribe(menuCommand => { + this.destroyMenu(menuCommand); })) } @@ -386,6 +389,10 @@ export class MenuScene extends Phaser.Scene { } } + public destroyMenu(menu: string) { + this.menuElement.getChildByID(menu).remove(); + } + public isDirty(): boolean { return false; } diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index f76c4218..61a3c890 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -17,9 +17,9 @@ import { DataLayerEvent, isDataLayerEvent } from "./Api/Events/DataLayerEvent"; import type { ITiledMap } from "./Phaser/Map/ITiledMap"; import type { MenuItemRegisterEvent } from "./Api/Events/MenuItemRegisterEvent"; import { isMenuItemClickedEvent } from "./Api/Events/MenuItemClickedEvent"; -import type {PlaySoundEvent} from "./Api/Events/PlaySoundEvent"; -import type {StopSoundEvent} from "./Api/Events/StopSoundEvent"; -import type {LoadSoundEvent} from "./Api/Events/LoadSoundEvent"; +import type { PlaySoundEvent } from "./Api/Events/PlaySoundEvent"; +import type { StopSoundEvent } from "./Api/Events/StopSoundEvent"; +import type { LoadSoundEvent } from "./Api/Events/LoadSoundEvent"; import SoundConfig = Phaser.Types.Sound.SoundConfig; interface WorkAdventureApi { @@ -47,8 +47,6 @@ interface WorkAdventureApi { registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void getCurrentUser(): Promise getCurrentRoom(): Promise - //loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void; - onPlayerMove(callback: (playerMovedEvent: HasPlayerMovedEvent) => void): void } @@ -176,7 +174,6 @@ const gameStateResolver: Array<(event: GameStateEvent) => void> = [] const dataLayerResolver: Array<(event: DataLayerEvent) => void> = [] let immutableData: GameStateEvent; -//const callbackPlayerMoved: { [type: string]: HasPlayerMovedEventCallback | ((arg?: HasPlayerMovedEvent | never) => void) } = {} const callbackPlayerMoved: Array<(event: HasPlayerMovedEvent) => void> = [] function postToParent(content: IframeEvent) { @@ -193,20 +190,6 @@ window.WA = { }) }, -/* loadTileset(name: string, imgUrl : string, tilewidth : number, tileheight : number, margin : number, spacing : number): void { - postToParent({ - type: "tilsetEvent", - data: { - name: name, - imgUrl: imgUrl, - tilewidth: tilewidth, - tileheight: tileheight, - margin: margin, - spacing: spacing - } as TilesetEvent - }) - },*/ - getCurrentUser(): Promise { return getGameState().then((gameState) => { return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags}; @@ -353,7 +336,7 @@ window.WA = { return popup; }, - registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void { menuCallbacks.set(commandDescriptor, callback); window.parent.postMessage({ 'type': 'registerMenuCommand', diff --git a/maps/tests/index.html b/maps/tests/index.html index a17a3b5d..527b435f 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -82,6 +82,54 @@
Test energy consumption + + + Success Failure Pending + + + Testing add a custom menu by scripting API + + + + + Success Failure Pending + + + Testing return current room attributes by Scripting API (Need to test from current user) + + + + + Success Failure Pending + + + Testing return current user attributes by Scripting API + + + + + Success Failure Pending + + + Test listening player movement by Scripting API + + + + + Success Failure Pending + + + Testing set a property on a layer by Scripting API + + + + + Success Failure Pending + + + Testing show or hide a layer by Scripting API + + From 74dda8ab69715733c347c007c2668729afac1728 Mon Sep 17 00:00:00 2001 From: jonny Date: Sat, 19 Jun 2021 15:17:28 +0200 Subject: [PATCH 42/87] allow properties on tiles # Conflicts: # front/src/Phaser/Game/GameMap.ts # front/src/Phaser/Map/ITiledMap.ts --- front/src/Phaser/Game/GameMap.ts | 48 ++++++++++++++++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 5 ++++ front/src/Phaser/Map/ITiledMap.ts | 9 +++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 7c93a702..dd567585 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,5 +1,8 @@ import type {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap"; import {LayersIterator} from "../Map/LayersIterator"; +import { CustomVector, Vector2 } from '../../utility/vector'; +import type { ITiledMap, ITiledMapLayerProperty } from "../Map/ITiledMap"; +import { LayersIterator } from "../Map/LayersIterator"; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; @@ -11,12 +14,37 @@ export class GameMap { private key: number|undefined; private lastProperties = new Map(); private callbacks = new Map>(); + + /** + * tileset.firstgid => (Map>) + */ + private tileSetPropertyMap = new Map>>() public readonly layersIterator: LayersIterator; + public exitUrls: Array = [] + public constructor(private map: ITiledMap) { this.layersIterator = new LayersIterator(map); + + for (const tileset of map.tilesets) { + if (!this.tileSetPropertyMap.has(tileset.firstgid)) { + this.tileSetPropertyMap.set(tileset.firstgid, new Map()) + } + tileset?.tiles?.forEach(tile => { + if (tile.properties) { + this.tileSetPropertyMap.get(tileset.firstgid)?.set(tile.id, tile.properties) + tile.properties.forEach(prop => { + if (prop.name == "exitUrl" && typeof prop.value == "string") { + this.exitUrls.push(prop.value); + } + }) + } + }) + } } + + /** * Sets the position of the current player (in pixels) * This will trigger events if properties are changing. @@ -59,12 +87,15 @@ export class GameMap { const properties = new Map(); for (const layer of this.layersIterator) { + + let tileIndex: number | undefined = undefined; if (layer.type !== 'tilelayer') { continue; } const tiles = layer.data as number[]; if (tiles[key] == 0) { continue; + tileIndex = tiles[key] } // There is a tile in this layer, let's embed the properties if (layer.properties !== undefined) { @@ -75,6 +106,23 @@ export class GameMap { properties.set(layerProperty.name, layerProperty.value); } } + + if (tileIndex) { + const tileset = this.map.tilesets.find(tileset => tileset.firstgid + tileset.tilecount > (tileIndex as number)) + if (tileset) { + const tileProperties = this.tileSetPropertyMap.get(tileset?.firstgid)?.get(tileIndex - tileset.firstgid) + if (tileProperties) { + for (const property of tileProperties) { + if (property.value) { + properties.set(property.name, property.value) + } else if (properties.has(property.name)) { + properties.delete(property.name) + } + } + } + } + + } } return properties; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index b5876d5a..7d01de46 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -447,6 +447,11 @@ export class GameScene extends DirtyScene implements CenterListener { } } } + + this.gameMap.exitUrls.forEach(exitUrl => { + this.loadNextGame(exitUrl) + }) + if (depth === -2) { throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at. This layer cannot be contained in a group.'); } diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index c4828911..91a29397 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -167,7 +167,7 @@ export interface ITiledTileSet { tilewidth: number; transparentcolor: string; terrains: ITiledMapTerrain[]; - tiles: {[key: string]: { terrain: number[] }}; + tiles?: Array; /** * Refers to external tileset file (should be JSON) @@ -175,6 +175,13 @@ export interface ITiledTileSet { source: string; } +export interface ITile { + id: number, + type?: string + + properties?: Array +} + export interface ITiledMapTerrain { name: string; tile: number; From 27ccdf165c5c851e106d37ec4c713427f15131e4 Mon Sep 17 00:00:00 2001 From: jonny Date: Sat, 19 Jun 2021 15:24:27 +0200 Subject: [PATCH 43/87] fixed merge imports --- front/src/Phaser/Game/GameMap.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index dd567585..6148830e 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,6 +1,3 @@ -import type {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap"; -import {LayersIterator} from "../Map/LayersIterator"; -import { CustomVector, Vector2 } from '../../utility/vector'; import type { ITiledMap, ITiledMapLayerProperty } from "../Map/ITiledMap"; import { LayersIterator } from "../Map/LayersIterator"; @@ -11,8 +8,8 @@ export type PropertyChangeCallback = (newValue: string | number | boolean | unde * It is used to handle layer properties. */ export class GameMap { - private key: number|undefined; - private lastProperties = new Map(); + private key: number | undefined; + private lastProperties = new Map(); private callbacks = new Map>(); /** @@ -79,12 +76,12 @@ export class GameMap { } } - public getCurrentProperties(): Map { + public getCurrentProperties(): Map { return this.lastProperties; } - private getProperties(key: number): Map { - const properties = new Map(); + private getProperties(key: number): Map { + const properties = new Map(); for (const layer of this.layersIterator) { @@ -95,7 +92,6 @@ export class GameMap { const tiles = layer.data as number[]; if (tiles[key] == 0) { continue; - tileIndex = tiles[key] } // There is a tile in this layer, let's embed the properties if (layer.properties !== undefined) { From c1d9b2c9ed20ce0ff94a5f18d9b03a5955330020 Mon Sep 17 00:00:00 2001 From: jonny Date: Sat, 19 Jun 2021 15:41:58 +0200 Subject: [PATCH 44/87] coverted property map to object --- front/src/Phaser/Game/GameMap.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 6148830e..4fd5b832 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -12,10 +12,7 @@ export class GameMap { private lastProperties = new Map(); private callbacks = new Map>(); - /** - * tileset.firstgid => (Map>) - */ - private tileSetPropertyMap = new Map>>() + private tileSetPropertyMap: { [tilset_firstgid: number]: { [tile_id: number]: Array } } = {} public readonly layersIterator: LayersIterator; public exitUrls: Array = [] @@ -24,12 +21,12 @@ export class GameMap { this.layersIterator = new LayersIterator(map); for (const tileset of map.tilesets) { - if (!this.tileSetPropertyMap.has(tileset.firstgid)) { - this.tileSetPropertyMap.set(tileset.firstgid, new Map()) + if (!this.tileSetPropertyMap[tileset.firstgid]) { + this.tileSetPropertyMap[tileset.firstgid] = {} } tileset?.tiles?.forEach(tile => { if (tile.properties) { - this.tileSetPropertyMap.get(tileset.firstgid)?.set(tile.id, tile.properties) + this.tileSetPropertyMap[tileset.firstgid][tile.id] = tile.properties tile.properties.forEach(prop => { if (prop.name == "exitUrl" && typeof prop.value == "string") { this.exitUrls.push(prop.value); @@ -106,7 +103,7 @@ export class GameMap { if (tileIndex) { const tileset = this.map.tilesets.find(tileset => tileset.firstgid + tileset.tilecount > (tileIndex as number)) if (tileset) { - const tileProperties = this.tileSetPropertyMap.get(tileset?.firstgid)?.get(tileIndex - tileset.firstgid) + const tileProperties = this.tileSetPropertyMap[tileset?.firstgid][tileIndex - tileset.firstgid] if (tileProperties) { for (const property of tileProperties) { if (property.value) { From 92485a02cfed2eae25de45387e7334793e28e189 Mon Sep 17 00:00:00 2001 From: jonny Date: Sat, 19 Jun 2021 15:46:32 +0200 Subject: [PATCH 45/87] tileIndex setting got merged out --- front/src/Phaser/Game/GameMap.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 4fd5b832..23a4a5a1 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -81,15 +81,19 @@ export class GameMap { const properties = new Map(); for (const layer of this.layersIterator) { - - let tileIndex: number | undefined = undefined; if (layer.type !== 'tilelayer') { continue; } - const tiles = layer.data as number[]; - if (tiles[key] == 0) { - continue; + + let tileIndex: number | undefined = undefined; + if (layer.data) { + const tiles = layer.data as number[]; + if (tiles[key] == 0) { + continue; + } + tileIndex = tiles[key] } + // There is a tile in this layer, let's embed the properties if (layer.properties !== undefined) { for (const layerProperty of layer.properties) { @@ -104,15 +108,13 @@ export class GameMap { const tileset = this.map.tilesets.find(tileset => tileset.firstgid + tileset.tilecount > (tileIndex as number)) if (tileset) { const tileProperties = this.tileSetPropertyMap[tileset?.firstgid][tileIndex - tileset.firstgid] - if (tileProperties) { - for (const property of tileProperties) { - if (property.value) { - properties.set(property.name, property.value) - } else if (properties.has(property.name)) { - properties.delete(property.name) - } + tileProperties?.forEach(property => { + if (property.value) { + properties.set(property.name, property.value) + } else if (properties.has(property.name)) { + properties.delete(property.name) } - } + }) } } From 5bf943ce7771ab53140da41080d4a353c4afa7f3 Mon Sep 17 00:00:00 2001 From: jonny Date: Sun, 20 Jun 2021 19:14:04 +0200 Subject: [PATCH 46/87] converted cache to constant lookup time --- front/src/Phaser/Game/GameMap.ts | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 23a4a5a1..ffcee287 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -12,7 +12,7 @@ export class GameMap { private lastProperties = new Map(); private callbacks = new Map>(); - private tileSetPropertyMap: { [tilset_firstgid: number]: { [tile_id: number]: Array } } = {} + private tileSetPropertyMap: { [tile_index: number]: Array } = {} public readonly layersIterator: LayersIterator; public exitUrls: Array = [] @@ -21,12 +21,9 @@ export class GameMap { this.layersIterator = new LayersIterator(map); for (const tileset of map.tilesets) { - if (!this.tileSetPropertyMap[tileset.firstgid]) { - this.tileSetPropertyMap[tileset.firstgid] = {} - } tileset?.tiles?.forEach(tile => { if (tile.properties) { - this.tileSetPropertyMap[tileset.firstgid][tile.id] = tile.properties + this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties tile.properties.forEach(prop => { if (prop.name == "exitUrl" && typeof prop.value == "string") { this.exitUrls.push(prop.value); @@ -105,18 +102,13 @@ export class GameMap { } if (tileIndex) { - const tileset = this.map.tilesets.find(tileset => tileset.firstgid + tileset.tilecount > (tileIndex as number)) - if (tileset) { - const tileProperties = this.tileSetPropertyMap[tileset?.firstgid][tileIndex - tileset.firstgid] - tileProperties?.forEach(property => { - if (property.value) { - properties.set(property.name, property.value) - } else if (properties.has(property.name)) { - properties.delete(property.name) - } - }) - } - + this.tileSetPropertyMap[tileIndex]?.forEach(property => { + if (property.value) { + properties.set(property.name, property.value) + } else if (properties.has(property.name)) { + properties.delete(property.name) + } + }) } } From 7716fe4b62631548616b163db12ca46639015451 Mon Sep 17 00:00:00 2001 From: jonny Date: Sun, 20 Jun 2021 19:26:35 +0200 Subject: [PATCH 47/87] run templater on running start script --- front/package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/front/package.json b/front/package.json index 3b248076..ac9061cc 100644 --- a/front/package.json +++ b/front/package.json @@ -53,13 +53,14 @@ "standardized-audio-context": "^25.2.4" }, "scripts": { - "start": "run-p serve svelte-check-watch", + "start": "run-p templater serve svelte-check-watch", + "templater": "cross-env ./templater.sh", "serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack", "test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch", - "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"" + "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"" } } From 979ae73d8d1e5d3b45f52702ad72dba92ad1c864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 21 Jun 2021 17:00:16 +0200 Subject: [PATCH 48/87] Removing old website directory --- website/.dockerignore | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 website/.dockerignore diff --git a/website/.dockerignore b/website/.dockerignore deleted file mode 100644 index 576c21a2..00000000 --- a/website/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -/dist/ -/node_modules/ -/dist/bundle.js -/yarn-error.log -/Dockerfile From 232fd33ec8244beda87b915b78e56089b6ab46b5 Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 21 Jun 2021 17:19:27 +0200 Subject: [PATCH 49/87] Migrating ConsoleGlobalMessageManager in svelte --- .../ConsoleGlobalMessageManager.ts | 397 ------------------ .../Administration/GlobalMessageManager.ts | 6 +- front/src/Components/App.svelte | 7 + .../ConsoleGlobalMessageManager.svelte | 44 ++ .../InputTextGlobalMessage.svelte | 97 +++++ .../UploadAudioGlobalMessage.svelte | 93 ++++ front/src/Components/images/music-file.svg | 27 ++ front/src/Phaser/Game/GameScene.ts | 3 - front/src/Phaser/Menu/MenuScene.ts | 10 +- .../src/Phaser/UserInput/UserInputManager.ts | 5 + .../ConsoleGlobalMessageManagerStore.ts | 5 + front/src/Stores/UserInputStore.ts | 10 + front/style/index.scss | 1 + front/style/style.scss | 108 +---- front/style/svelte-style.scss | 82 ++++ 15 files changed, 386 insertions(+), 509 deletions(-) delete mode 100644 front/src/Administration/ConsoleGlobalMessageManager.ts create mode 100644 front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte create mode 100644 front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte create mode 100644 front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte create mode 100644 front/src/Components/images/music-file.svg create mode 100644 front/src/Stores/ConsoleGlobalMessageManagerStore.ts create mode 100644 front/src/Stores/UserInputStore.ts create mode 100644 front/style/svelte-style.scss diff --git a/front/src/Administration/ConsoleGlobalMessageManager.ts b/front/src/Administration/ConsoleGlobalMessageManager.ts deleted file mode 100644 index 0dbfe834..00000000 --- a/front/src/Administration/ConsoleGlobalMessageManager.ts +++ /dev/null @@ -1,397 +0,0 @@ -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import type {UserInputManager} from "../Phaser/UserInput/UserInputManager"; -import type {RoomConnection} from "../Connexion/RoomConnection"; -import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels"; -import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService"; - -export const CLASS_CONSOLE_MESSAGE = 'main-console'; -export const INPUT_CONSOLE_MESSAGE = 'input-send-text'; -export const UPLOAD_CONSOLE_MESSAGE = 'input-upload-music'; -export const INPUT_TYPE_CONSOLE = 'input-type'; -export const VIDEO_QUALITY_SELECT = 'select-video-quality'; - -export const AUDIO_TYPE = AdminMessageEventTypes.audio; -export const MESSAGE_TYPE = AdminMessageEventTypes.admin; - -interface EventTargetFiles extends EventTarget { - files: Array; -} - -/** - * @deprecated - */ -export class ConsoleGlobalMessageManager { - - private readonly divMainConsole: HTMLDivElement; - private readonly divMessageConsole: HTMLDivElement; - //private readonly divSettingConsole: HTMLDivElement; - private readonly buttonMainConsole: HTMLDivElement; - private readonly buttonSendMainConsole: HTMLImageElement; - //private readonly buttonAdminMainConsole: HTMLImageElement; - //private readonly buttonSettingsMainConsole: HTMLImageElement; - private activeConsole: boolean = false; - private activeMessage: boolean = false; - private activeSetting: boolean = false; - private userInputManager!: UserInputManager; - private static cssLoaded: boolean = false; - - constructor(private Connection: RoomConnection, userInputManager : UserInputManager, private isAdmin: Boolean) { - this.buttonMainConsole = document.createElement('div'); - this.buttonMainConsole.classList.add('console'); - this.buttonMainConsole.hidden = true; - this.divMainConsole = document.createElement('div'); - this.divMainConsole.className = CLASS_CONSOLE_MESSAGE; - this.divMessageConsole = document.createElement('div'); - this.divMessageConsole.className = 'message'; - //this.divSettingConsole = document.createElement('div'); - //this.divSettingConsole.className = 'setting'; - this.buttonSendMainConsole = document.createElement('img'); - this.buttonSendMainConsole.id = 'btn-send-message'; - //this.buttonSettingsMainConsole = document.createElement('img'); - //this.buttonAdminMainConsole = document.createElement('img'); - this.userInputManager = userInputManager; - this.initialise(); - - } - - initialise() { - for (const elem of document.getElementsByClassName(CLASS_CONSOLE_MESSAGE)) { - elem.remove(); - } - - const typeConsole = document.createElement('input'); - typeConsole.id = INPUT_TYPE_CONSOLE; - typeConsole.value = MESSAGE_TYPE; - typeConsole.type = 'hidden'; - - const menu = document.createElement('div'); - menu.classList.add('menu') - const textMessage = document.createElement('span'); - textMessage.innerText = "Message"; - textMessage.classList.add('active'); - textMessage.addEventListener('click', () => { - textMessage.classList.add('active'); - const messageSection = HtmlUtils.getElementByIdOrFail(this.getSectionId(INPUT_CONSOLE_MESSAGE)); - messageSection.classList.add('active'); - - textAudio.classList.remove('active'); - const audioSection = HtmlUtils.getElementByIdOrFail(this.getSectionId(UPLOAD_CONSOLE_MESSAGE)); - audioSection.classList.remove('active'); - - typeConsole.value = MESSAGE_TYPE; - }); - menu.appendChild(textMessage); - const textAudio = document.createElement('span'); - textAudio.innerText = "Audio"; - textAudio.addEventListener('click', () => { - textAudio.classList.add('active'); - const audioSection = HtmlUtils.getElementByIdOrFail(this.getSectionId(UPLOAD_CONSOLE_MESSAGE)); - audioSection.classList.add('active'); - - textMessage.classList.remove('active'); - const messageSection = HtmlUtils.getElementByIdOrFail(this.getSectionId(INPUT_CONSOLE_MESSAGE)); - messageSection.classList.remove('active'); - - typeConsole.value = AUDIO_TYPE; - }); - menu.appendChild(textMessage); - menu.appendChild(textAudio); - this.divMessageConsole.appendChild(menu); - - this.buttonSendMainConsole.src = 'resources/logos/send-yellow.svg'; - this.buttonSendMainConsole.addEventListener('click', () => { - if(this.activeMessage){ - this.disabledMessageConsole(); - }else{ - this.activeMessageConsole(); - } - }); - - /*this.buttonAdminMainConsole.src = 'resources/logos/setting-yellow.svg'; - this.buttonAdminMainConsole.addEventListener('click', () => { - window.open(ADMIN_URL, '_blank'); - });*/ - - /*this.buttonSettingsMainConsole.src = 'resources/logos/monitor-yellow.svg'; - this.buttonSettingsMainConsole.addEventListener('click', () => { - if(this.activeSetting){ - this.disabledSettingConsole(); - }else{ - this.activeSettingConsole(); - } - });*/ - - this.divMessageConsole.appendChild(typeConsole); - - /*if(this.isAdmin) { - this.buttonMainConsole.appendChild(this.buttonSendMainConsole); - //this.buttonMainConsole.appendChild(this.buttonAdminMainConsole); - }*/ - this.createTextMessagePart(); - this.createUploadAudioPart(); - //this.buttonMainConsole.appendChild(this.buttonSettingsMainConsole); - - this.divMainConsole.appendChild(this.buttonMainConsole); - this.divMainConsole.appendChild(this.divMessageConsole); - //this.divMainConsole.appendChild(this.divSettingConsole); - - const mainSectionDiv = HtmlUtils.getElementByIdOrFail('main-container'); - mainSectionDiv.appendChild(this.divMainConsole); - } - - createTextMessagePart(){ - const div = document.createElement('div'); - div.id = INPUT_CONSOLE_MESSAGE - const buttonSend = document.createElement('button'); - buttonSend.innerText = 'Send'; - buttonSend.classList.add('btn'); - buttonSend.addEventListener('click', (event: MouseEvent) => { - this.sendMessage(); - this.disabledMessageConsole(); - }); - const buttonDiv = document.createElement('div'); - buttonDiv.classList.add('btn-action'); - buttonDiv.appendChild(buttonSend) - - const section = document.createElement('section'); - section.id = this.getSectionId(INPUT_CONSOLE_MESSAGE); - section.classList.add('active'); - section.appendChild(div); - section.appendChild(buttonDiv); - this.divMessageConsole.appendChild(section); - - (async () => { - try{ - // Start loading CSS - const cssPromise = ConsoleGlobalMessageManager.loadCss(); - // Import quill - const {default: Quill}:any = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any - // Wait for CSS to be loaded - await cssPromise; - - const toolbarOptions = [ - ['bold', 'italic', 'underline', 'strike'], // toggled buttons - ['blockquote', 'code-block'], - - [{'header': 1}, {'header': 2}], // custom button values - [{'list': 'ordered'}, {'list': 'bullet'}], - [{'script': 'sub'}, {'script': 'super'}], // superscript/subscript - [{'indent': '-1'}, {'indent': '+1'}], // outdent/indent - [{'direction': 'rtl'}], // text direction - - [{'size': ['small', false, 'large', 'huge']}], // custom dropdown - [{'header': [1, 2, 3, 4, 5, 6, false]}], - - [{'color': []}, {'background': []}], // dropdown with defaults from theme - [{'font': []}], - [{'align': []}], - - ['clean'], - - ['link', 'image', 'video'] - // remove formatting button - ]; - - new Quill(`#${INPUT_CONSOLE_MESSAGE}`, { - theme: 'snow', - modules: { - toolbar: toolbarOptions - }, - }); - }catch(err){ - console.error(err); - } - })(); - } - - createUploadAudioPart(){ - const div = document.createElement('div'); - div.classList.add('upload'); - - const label = document.createElement('label'); - label.setAttribute('for', UPLOAD_CONSOLE_MESSAGE); - - const img = document.createElement('img'); - img.setAttribute('for', UPLOAD_CONSOLE_MESSAGE); - img.src = 'resources/logos/music-file.svg'; - - const input = document.createElement('input'); - input.type = 'file'; - input.id = UPLOAD_CONSOLE_MESSAGE - input.addEventListener('input', (e: Event) => { - if(!e.target){ - return; - } - const eventTarget : EventTargetFiles = (e.target as EventTargetFiles); - if(!eventTarget || !eventTarget.files || eventTarget.files.length === 0){ - return; - } - const file : File = eventTarget.files[0]; - - if(!file){ - return; - } - - try { - HtmlUtils.removeElementByIdOrFail('audi-message-filename'); - }catch (err) { - console.error(err) - } - - const p = document.createElement('p'); - p.id = 'audi-message-filename'; - p.innerText = `${file.name} : ${this.getFileSize(file.size)}`; - label.appendChild(p); - }); - - label.appendChild(img); - div.appendChild(label); - div.appendChild(input); - - const buttonSend = document.createElement('button'); - buttonSend.innerText = 'Send'; - buttonSend.classList.add('btn'); - buttonSend.addEventListener('click', (event: MouseEvent) => { - this.sendMessage(); - this.disabledMessageConsole(); - }); - const buttonDiv = document.createElement('div'); - buttonDiv.classList.add('btn-action'); - buttonDiv.appendChild(buttonSend) - - const section = document.createElement('section'); - section.id = this.getSectionId(UPLOAD_CONSOLE_MESSAGE); - section.appendChild(div); - section.appendChild(buttonDiv); - this.divMessageConsole.appendChild(section); - } - - private static loadCss(): Promise { - return new Promise((resolve, reject) => { - if (ConsoleGlobalMessageManager.cssLoaded) { - resolve(); - return; - } - const fileref = document.createElement("link") - fileref.setAttribute("rel", "stylesheet") - fileref.setAttribute("type", "text/css") - fileref.setAttribute("href", "https://cdn.quilljs.com/1.3.7/quill.snow.css"); - document.getElementsByTagName("head")[0].appendChild(fileref); - ConsoleGlobalMessageManager.cssLoaded = true; - fileref.onload = () => { - resolve(); - } - fileref.onerror = () => { - reject(); - } - }); - } - - sendMessage(){ - const inputType = HtmlUtils.getElementByIdOrFail(INPUT_TYPE_CONSOLE); - if(AUDIO_TYPE !== inputType.value && MESSAGE_TYPE !== inputType.value){ - throw "Error event type"; - } - if(AUDIO_TYPE === inputType.value){ - return this.sendAudioMessage(); - } - return this.sendTextMessage(); - } - - private sendTextMessage(){ - const elements = document.getElementsByClassName('ql-editor'); - const quillEditor = elements.item(0); - if(!quillEditor){ - throw "Error get quill node"; - } - const GlobalMessage : PlayGlobalMessageInterface = { - id: "1", // FIXME: use another ID? - message: quillEditor.innerHTML, - type: MESSAGE_TYPE - }; - quillEditor.innerHTML = ''; - this.Connection.emitGlobalMessage(GlobalMessage); - } - - private async sendAudioMessage(){ - const inputAudio = HtmlUtils.getElementByIdOrFail(UPLOAD_CONSOLE_MESSAGE); - const selectedFile = inputAudio.files ? inputAudio.files[0] : null; - if(!selectedFile){ - throw 'no file selected'; - } - - const fd = new FormData(); - fd.append('file', selectedFile); - const res = await this.Connection.uploadAudio(fd); - - const GlobalMessage : PlayGlobalMessageInterface = { - id: (res as {id: string}).id, - message: (res as {path: string}).path, - type: AUDIO_TYPE - }; - inputAudio.value = ''; - try { - HtmlUtils.removeElementByIdOrFail('audi-message-filename'); - }catch (err) { - console.error(err); - } - this.Connection.emitGlobalMessage(GlobalMessage); - } - - active(){ - this.userInputManager.disableControls(); - this.divMainConsole.style.top = '0'; - this.activeConsole = true; - } - - disabled(){ - this.userInputManager.initKeyBoardEvent(); - this.activeConsole = false; - this.divMainConsole.style.top = '-80%'; - } - - activeMessageConsole(){ - if(!this.isAdmin){ - throw "User is not admin"; - } - if(this.activeMessage){ - this.disabledMessageConsole(); - return; - } - this.activeMessage = true; - this.active(); - this.divMessageConsole.classList.add('active'); - this.buttonMainConsole.hidden = false; - this.buttonSendMainConsole.classList.add('active'); - //if button not - try{ - HtmlUtils.getElementByIdOrFail('btn-send-message'); - }catch (e) { - this.buttonMainConsole.appendChild(this.buttonSendMainConsole); - } - } - - disabledMessageConsole(){ - this.activeMessage = false; - this.disabled(); - this.buttonMainConsole.hidden = true; - this.divMessageConsole.classList.remove('active'); - this.buttonSendMainConsole.classList.remove('active'); - } - - private getSectionId(id: string) : string { - return `section-${id}`; - } - - private getFileSize(number: number) :string { - if (number < 1024) { - return number + 'bytes'; - } else if (number >= 1024 && number < 1048576) { - return (number / 1024).toFixed(1) + 'KB'; - } else if (number >= 1048576) { - return (number / 1048576).toFixed(1) + 'MB'; - }else{ - return ''; - } - } -} diff --git a/front/src/Administration/GlobalMessageManager.ts b/front/src/Administration/GlobalMessageManager.ts index 1500a6ec..aae695fa 100644 --- a/front/src/Administration/GlobalMessageManager.ts +++ b/front/src/Administration/GlobalMessageManager.ts @@ -1,10 +1,10 @@ import {HtmlUtils} from "./../WebRtc/HtmlUtils"; -import {AUDIO_TYPE, MESSAGE_TYPE} from "./ConsoleGlobalMessageManager"; import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; import type {RoomConnection} from "../Connexion/RoomConnection"; import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels"; import {soundPlayingStore} from "../Stores/SoundPlayingStore"; import {soundManager} from "../Phaser/Game/SoundManager"; +import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService"; export class GlobalMessageManager { @@ -36,11 +36,11 @@ export class GlobalMessageManager { previousMessage.remove(); } - if(AUDIO_TYPE === message.type){ + if(AdminMessageEventTypes.audio === message.type){ this.playAudioMessage(message.id, message.message); } - if(MESSAGE_TYPE === message.type){ + if(AdminMessageEventTypes.admin === message.type){ this.playTextMessage(message.id, message.message); } } diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index a39f2dc7..448ac015 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -23,6 +23,8 @@ import AudioPlaying from "./UI/AudioPlaying.svelte"; import {soundPlayingStore} from "../Stores/SoundPlayingStore"; import ErrorDialog from "./UI/ErrorDialog.svelte"; + import {ConsoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore"; + import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte"; export let game: Game; @@ -72,6 +74,11 @@ {/if} + {#if $ConsoleGlobalMessageManagerVisibleStore} +
+ +
+ {/if} {#if $helpCameraSettingsVisibleStore}
diff --git a/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte b/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte new file mode 100644 index 00000000..93385419 --- /dev/null +++ b/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte @@ -0,0 +1,44 @@ + + + +
+ +
+

Global Message

+
+ +
+ {#if inputSendTextActive} + + {/if} + {#if uploadMusicActive} + + {/if} +
+
+
+
\ No newline at end of file diff --git a/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte b/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte new file mode 100644 index 00000000..089c3254 --- /dev/null +++ b/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte @@ -0,0 +1,97 @@ + + + +
+
+
+ +
+
+ + + diff --git a/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte b/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte new file mode 100644 index 00000000..459d7273 --- /dev/null +++ b/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte @@ -0,0 +1,93 @@ + + + +
+
+ Upload a file {fileinput.click();}}> + {#if filename != undefined} + + {/if} + {inputAudioFile(e)}}> +
+
+ +
+
\ No newline at end of file diff --git a/front/src/Components/images/music-file.svg b/front/src/Components/images/music-file.svg new file mode 100644 index 00000000..a97656ba --- /dev/null +++ b/front/src/Components/images/music-file.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 72279c61..5913d718 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -58,7 +58,6 @@ import {connectionManager} from "../../Connexion/ConnectionManager"; import type {RoomConnection} from "../../Connexion/RoomConnection"; import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; import {userMessageManager} from "../../Administration/UserMessageManager"; -import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; import {ResizableScene} from "../Login/ResizableScene"; import {Room} from "../../Connexion/Room"; import {jitsiFactory} from "../../WebRtc/JitsiFactory"; @@ -152,7 +151,6 @@ export class GameScene extends DirtyScene implements CenterListener { public connection: RoomConnection|undefined; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; - public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; private connectionAnswerPromise: Promise; private connectionAnswerPromiseResolve!: (value: RoomJoinedMessageInterface | PromiseLike) => void; // A promise that will resolve when the "create" method is called (signaling loading is ended) @@ -676,7 +674,6 @@ export class GameScene extends DirtyScene implements CenterListener { //this.initUsersPosition(roomJoinedMessage.users); this.connectionAnswerPromiseResolve(onConnect.room); // Analyze tags to find if we are admin. If yes, show console. - this.ConsoleGlobalMessageManager = new ConsoleGlobalMessageManager(this.connection, this.userInputManager, this.connection.isAdmin()); this.scene.wake(); diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 54fa395a..2d848467 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -11,6 +11,8 @@ import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Co import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {menuIconVisible} from "../../Stores/MenuStore"; import {videoConstraintStore} from "../../Stores/MediaStore"; +import {ConsoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; +import {get} from "svelte/store"; export const MenuSceneName = 'MenuScene'; const gameMenuKey = 'gameMenu'; @@ -159,7 +161,7 @@ export class MenuScene extends Phaser.Scene { this.sideMenuOpened = false; this.closeAll(); this.menuButton.getChildByID('openMenuButton').innerHTML = ``; - gameManager.getCurrentGameScene(this).ConsoleGlobalMessageManager.disabledMessageConsole(); + ConsoleGlobalMessageManagerVisibleStore.set(false); this.tweens.add({ targets: this.menuElement, x: closedSideMenuX, @@ -304,7 +306,11 @@ export class MenuScene extends Phaser.Scene { this.toggleFullscreen(); break; case 'adminConsoleButton': - gameManager.getCurrentGameScene(this).ConsoleGlobalMessageManager.activeMessageConsole(); + if (get(ConsoleGlobalMessageManagerVisibleStore)) { + ConsoleGlobalMessageManagerVisibleStore.set(false); + } else { + ConsoleGlobalMessageManagerVisibleStore.set(true); + } break; } } diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index 70bb9b1b..068e84a2 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -2,6 +2,7 @@ import type { Direction } from "../../types"; import type {GameScene} from "../Game/GameScene"; import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {MobileJoystick} from "../Components/MobileJoystick"; +import {enableUserInputsStore} from "../../Stores/UserInputStore"; interface UserInputManagerDatum { keyInstance: Phaser.Input.Keyboard.Key; @@ -58,6 +59,10 @@ export class UserInputManager { if (touchScreenManager.supportTouchScreen) { this.initVirtualJoystick(); } + + enableUserInputsStore.subscribe((enable) => { + enable ? this.restoreControls() : this.disableControls() + }) } initVirtualJoystick() { diff --git a/front/src/Stores/ConsoleGlobalMessageManagerStore.ts b/front/src/Stores/ConsoleGlobalMessageManagerStore.ts new file mode 100644 index 00000000..4c557d71 --- /dev/null +++ b/front/src/Stores/ConsoleGlobalMessageManagerStore.ts @@ -0,0 +1,5 @@ +import { writable } from "svelte/store"; + +export const ConsoleGlobalMessageManagerVisibleStore = writable(false); + +export const ConsoleGlobalMessageManagerFocusStore = writable(false); \ No newline at end of file diff --git a/front/src/Stores/UserInputStore.ts b/front/src/Stores/UserInputStore.ts new file mode 100644 index 00000000..b5b4de30 --- /dev/null +++ b/front/src/Stores/UserInputStore.ts @@ -0,0 +1,10 @@ +import {derived} from "svelte/store"; +import {ConsoleGlobalMessageManagerFocusStore,} from "./ConsoleGlobalMessageManagerStore"; + +//derived from the focus on Menu, ConsoleGlobal, Chat and ... +export const enableUserInputsStore = derived( + ConsoleGlobalMessageManagerFocusStore, + ($ConsoleGlobalMessageManagerFocusStore) => { + return !$ConsoleGlobalMessageManagerFocusStore; + } +); \ No newline at end of file diff --git a/front/style/index.scss b/front/style/index.scss index 7ed141cd..a6afa557 100644 --- a/front/style/index.scss +++ b/front/style/index.scss @@ -3,3 +3,4 @@ @import "style"; @import "mobile-style.scss"; @import "fonts.scss"; +@import "svelte-style.scss"; diff --git a/front/style/style.scss b/front/style/style.scss index beed1db5..5c958309 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -627,17 +627,15 @@ input[type=range]:focus::-ms-fill-upper { grid-template-columns: repeat(4, 1fr); } -/*CONSOLE*/ - -.message-container, -.main-console{ +/*GLOBAL MESSAGE*/ +.message-container { position: absolute; width: 80%; height: 80%; min-height: 200px; max-height: 80%; top: -80%; - /*left: 10%;*/ + //left: 10%; left: 250px; background: #333333; z-index: 200; @@ -660,7 +658,6 @@ input[type=range]:focus::-ms-fill-upper { max-height: 400px; } -.main-console div.console, .message-container div.clear { position: absolute; color: white; @@ -675,22 +672,11 @@ input[type=range]:focus::-ms-fill-upper { text-align: center; } -.main-console div.message, -.main-console div.setting{ - display: none; -} - -.main-console div.message.active, -.main-console div.setting.active{ - display: block; -} - .message-container div.clear{ width: 100px; left: calc(50% - 50px); } -.main-console div.console img, .message-container div.clear img{ margin-top: 6px; width: 30px; @@ -701,112 +687,26 @@ input[type=range]:focus::-ms-fill-upper { transform: rotateY(0); opacity: 0.5; } -.main-console div.console img:hover, + .message-container div.clear img:hover{ opacity: 1; } -.main-console div.console img.active, .message-container div.clear img{ transform: rotateY(3.142rad); opacity: 1; } -.main-console div.console p, .message-container div.clear p{ margin-top: 12px; } -.main-console div.console:hover, .message-container div.clear:hover { cursor: url('./images/cursor_pointer.png'), pointer; top: calc(100% + 5px); transform: scale(1.2) translateY(3px); } -.main-console #input-send-text{ - min-height: 200px; -} - -.main-console #input-send-text .ql-editor{ - color: white; - min-height: 200px; - max-height: 300px; -} - -.main-console .ql-toolbar{ - background: white; -} - -.main-console .btn-action{ - margin: 10px; - text-align: center; -} - -.main-console .btn-action .btn{ - border: 1px solid black; - background-color: #00000000; - color: #ffda01; - border-radius: 15px; - padding: 10px 30px; - transition: all .2s ease; -} -.main-console .btn-action .btn:hover{ - cursor: url('./images/cursor_pointer.png'), pointer; - background-color: #ffda01; - color: black; - border: 1px solid black; - transform: scale(1.1); -} - -.main-console .menu { - padding: 20px; - color: #ffffffa6; - text-align: center; -} - -.main-console .menu span { - margin: 20px; - cursor: url('./images/cursor_pointer.png'), pointer; -} - -.main-console .menu span.active { - color: white; - border-bottom: solid 1px white; -} - -.main-console section{ - text-align: center; - display: none; -} - -.main-console section.active{ - display: block; -} - -.main-console section div.upload{ - text-align: center; - border: solid 1px #ffda01; - height: 150px; - margin: 10px 200px; - padding: 20px; - min-height: 200px; -} - -.main-console section div.upload label{ - color: #ffda01; -} -.main-console section div.upload input{ - display: none; -} -.main-console section div.upload label img{ - height: 150px; - cursor: url('./images/cursor_pointer.png'), pointer; -} -.main-console section div.upload label img{ - cursor: url('./images/cursor_pointer.png'), pointer; -} - /* VIDEO QUALITY */ .main-console div.setting h1{ diff --git a/front/style/svelte-style.scss b/front/style/svelte-style.scss new file mode 100644 index 00000000..bb85fb2b --- /dev/null +++ b/front/style/svelte-style.scss @@ -0,0 +1,82 @@ +//Contains all styles not unique to a svelte component. + +//ConsoleGlobalMessage +div.main-console.nes-container { + pointer-events: auto; + margin-left: auto; + margin-right: auto; + top: 20vh; + width: 50vw; + height: 50vh; + padding: 0; + background-color: #333333; + + .btn-action{ + margin: 10px; + text-align: center; + } + + .main-global-message { + width: 100%; + max-height: 100%; + } + + .main-global-message h2 { + text-align: center; + color: white; + } + + div.global-message { + display: flex; + max-height: 100%; + width: 100%; + } + + div.menu { + flex: auto; + } + + div.menu button { + margin: 7px; + } + + .main-input { + width: 95%; + } + +//InputTextGlobalMessage + .section-input-send-text { + margin: 10px; + } + + .section-input-send-text .input-send-text .ql-editor{ + color: white; + min-height: 200px; + } + + .section-input-send-text .ql-toolbar{ + background: white; + } + +//UploadAudioGlobalMessage + .section-input-send-audio { + margin: 10px; + } + + .section-input-send-audio .input-send-audio { + text-align: center; + } + + .section-input-send-audio #input-send-audio{ + display: none; + } + + .section-input-send-audio div.input-send-audio label{ + color: white; + } + + .section-input-send-audio div.input-send-audio img{ + height: 150px; + cursor: url('./images/cursor_pointer.png'), pointer; + } +} From 42c0d1002f2a663c9e2f3b2d046a190fa80c3a92 Mon Sep 17 00:00:00 2001 From: kharhamel Date: Mon, 21 Jun 2021 17:58:04 +0200 Subject: [PATCH 50/87] FIX: no more whitespaces in player names --- front/src/Connexion/LocalUser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/Connexion/LocalUser.ts b/front/src/Connexion/LocalUser.ts index c877d119..d240a90e 100644 --- a/front/src/Connexion/LocalUser.ts +++ b/front/src/Connexion/LocalUser.ts @@ -10,7 +10,7 @@ export interface CharacterTexture { export const maxUserNameLength: number = MAX_USERNAME_LENGTH; export function isUserNameValid(value: unknown): boolean { - return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && value.indexOf(' ') === -1; + return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && /\S/.test(value); } export function areCharacterLayersValid(value: string[] | null): boolean { From ba1bcf226abbd0bcc46419a328a87fe442cdb3dc Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 21 Jun 2021 18:22:31 +0200 Subject: [PATCH 51/87] menu command api --- docs/maps/api-ui.md | 23 ++ docs/maps/assets/menu-command.png | Bin 0 -> 9856 bytes front/src/Api/Events/IframeEvent.ts | 8 +- .../src/Api/Events/ui/MenuItemClickedEvent.ts | 21 + .../Api/Events/ui/MenuItemRegisterEvent.ts | 25 ++ front/src/Api/IframeListener.ts | 33 +- front/src/Api/iframe/ui.ts | 29 +- front/src/Phaser/Game/GameScene.ts | 387 +++++++++--------- front/src/Phaser/Menu/MenuScene.ts | 107 +++-- 9 files changed, 381 insertions(+), 252 deletions(-) create mode 100644 docs/maps/assets/menu-command.png create mode 100644 front/src/Api/Events/ui/MenuItemClickedEvent.ts create mode 100644 front/src/Api/Events/ui/MenuItemRegisterEvent.ts diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index edda8613..572de593 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -65,3 +65,26 @@ WA.room.onLeaveZone('myZone', () => { helloWorldPopup.close(); }); ``` + +### register additional menu entries + +adds an additional Entry to the main menu , these exist until the map is unloaded + + +```typescript +registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void +``` +Example: + + +```javascript + +WA.registerMenuCommand("test", () => { + WA.sendChatMessage("test clicked", "menu cmd") +}) + +``` + +
+ +
\ No newline at end of file diff --git a/docs/maps/assets/menu-command.png b/docs/maps/assets/menu-command.png new file mode 100644 index 0000000000000000000000000000000000000000..0caf75c9d772d63e43365040e033d3f93e69cf3c GIT binary patch literal 9856 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 Ho$&t!s2Avf literal 0 HcmV?d00001 diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 4da8ea96..80fc5850 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -6,12 +6,14 @@ import type { ClosePopupEvent } from './ClosePopupEvent'; import type { EnterLeaveEvent } from './EnterLeaveEvent'; import type { GoToPageEvent } from './GoToPageEvent'; import type { LoadPageEvent } from './LoadPageEvent'; +import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenTabEvent } from './OpenTabEvent'; +import type { PlaySoundEvent } from "./PlaySoundEvent"; +import type { MenuItemClickedEvent } from './ui/MenuItemClickedEvent'; +import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent'; import type { UserInputChatEvent } from './UserInputChatEvent'; -import type { LoadSoundEvent} from "./LoadSoundEvent"; -import type {PlaySoundEvent} from "./PlaySoundEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -36,6 +38,7 @@ export type IframeEventMap = { loadSound: LoadSoundEvent playSound: PlaySoundEvent stopSound: null, + registerMenuCommand: MenuItemRegisterEvent } export interface IframeEvent { type: T; @@ -52,6 +55,7 @@ export interface IframeResponseEventMap { leaveEvent: EnterLeaveEvent buttonClickedEvent: ButtonClickedEvent // gameState: GameStateEvent + menuItemClicked: MenuItemClickedEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/ui/MenuItemClickedEvent.ts b/front/src/Api/Events/ui/MenuItemClickedEvent.ts new file mode 100644 index 00000000..6444cb09 --- /dev/null +++ b/front/src/Api/Events/ui/MenuItemClickedEvent.ts @@ -0,0 +1,21 @@ +import * as tg from "generic-type-guard"; +import { iframeListener } from '../../IframeListener'; + +export const isMenuItemClickedEvent = + new tg.IsInterface().withProperties({ + menuItem: tg.isString + }).get(); +/** + * A message sent from the game to the iFrame when a menu item is clicked. + */ +export type MenuItemClickedEvent = tg.GuardedType; + + +export function sendMenuClickedEvent(menuItem: string) { + iframeListener.postMessage({ + 'type': 'menuItemClicked', + 'data': { + menuItem: menuItem, + } as MenuItemClickedEvent + }); +} \ No newline at end of file diff --git a/front/src/Api/Events/ui/MenuItemRegisterEvent.ts b/front/src/Api/Events/ui/MenuItemRegisterEvent.ts new file mode 100644 index 00000000..4a56d8a0 --- /dev/null +++ b/front/src/Api/Events/ui/MenuItemRegisterEvent.ts @@ -0,0 +1,25 @@ +import * as tg from "generic-type-guard"; +import { Subject } from 'rxjs'; + +export const isMenuItemRegisterEvent = + new tg.IsInterface().withProperties({ + menutItem: tg.isString + }).get(); +/** + * A message sent from the iFrame to the game to add a new menu item. + */ +export type MenuItemRegisterEvent = tg.GuardedType; + +export const isMenuItemRegisterIframeEvent = + new tg.IsInterface().withProperties({ + type: tg.isSingletonString("registerMenuCommand"), + data: isMenuItemRegisterEvent + }).get(); + + +const _registerMenuCommandStream: Subject = new Subject(); +export const registerMenuCommandStream = _registerMenuCommandStream.asObservable(); + +export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) { + _registerMenuCommandStream.next(event.menutItem) +} \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 232502a1..11852358 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,21 +1,22 @@ import { Subject } from "rxjs"; -import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; import { HtmlUtils } from "../WebRtc/HtmlUtils"; +import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; +import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; +import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; +import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; +import { IframeEvent, IframeEventMap, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; +import { isLoadPageEvent } from './Events/LoadPageEvent'; +import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; +import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; -import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; -import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; -import { scriptUtils } from "./ScriptUtils"; -import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; -import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; -import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; +import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; +import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent"; +import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from './Events/ui/MenuItemRegisterEvent'; import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; -import { isLoadPageEvent } from './Events/LoadPageEvent'; -import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; -import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; -import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; +import { scriptUtils } from "./ScriptUtils"; /** * Listens to messages from iframes and turn those messages into easy to use observables. * Also allows to send messages to those iframes. @@ -33,7 +34,7 @@ class IframeListener { private readonly _goToPageStream: Subject = new Subject(); public readonly goToPageStream = this._goToPageStream.asObservable(); - + private readonly _loadPageStream: Subject = new Subject(); public readonly loadPageStream = this._loadPageStream.asObservable(); @@ -137,9 +138,11 @@ class IframeListener { } else if (payload.type === 'removeBubble') { this._removeBubbleStream.next(); - }else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){ + } else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { this._loadPageStream.next(payload.data.url); - } + } else if (isMenuItemRegisterIframeEvent(payload)) [ + handleMenuItemRegistrationEvent(payload.data) + ] } @@ -263,7 +266,7 @@ class IframeListener { /** * Sends the message... to all allowed iframes. */ - private postMessage(message: IframeResponseEvent) { + public postMessage(message: IframeResponseEvent) { for (const iframe of this.iframes) { iframe.contentWindow?.postMessage(message, '*'); } diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index 629d3c36..8e9943b2 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -1,14 +1,17 @@ import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; -import type { ClosePopupEvent } from '../Events/ClosePopupEvent'; +import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent'; +import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { apiCallback } from "./registeredCallbacks"; -import {Popup} from "./Ui/Popup"; -import type {ButtonClickedCallback, ButtonDescriptor} from "./Ui/ButtonDescriptor"; +import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor"; +import { Popup } from "./Ui/Popup"; let popupId = 0; const popups: Map = new Map(); const popupCallbacks: Map> = new Map>(); +const menuCallbacks: Map void> = new Map() + interface ZonedPopupOptions { zone: string objectLayerName?: string, @@ -33,6 +36,16 @@ class WorkAdventureUiCommands extends IframeApiContribution { + const callback = menuCallbacks.get(event.menuItem); + if (callback) { + callback(event.menuItem) + } + } })]; @@ -71,6 +84,16 @@ class WorkAdventureUiCommands extends IframeApiContribution void) { + menuCallbacks.set(commandDescriptor, callback); + sendToWorkadventure({ + 'type': 'registerMenuCommand', + 'data': { + menutItem: commandDescriptor + } as MenuItemRegisterEvent + }); + } + displayBubble(): void { sendToWorkadventure({ 'type': 'displayBubble', data: null }); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index b5876d5a..317b27bb 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,10 @@ -import {gameManager, HasMovedEvent} from "./GameManager"; +import { Queue } from 'queue-typescript'; +import type { Subscription } from "rxjs"; +import { ConsoleGlobalMessageManager } from "../../Administration/ConsoleGlobalMessageManager"; +import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; +import { userMessageManager } from "../../Administration/UserMessageManager"; +import { iframeListener } from "../../Api/IframeListener"; +import { connectionManager } from "../../Connexion/ConnectionManager"; import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -9,13 +15,50 @@ import type { PositionInterface, RoomJoinedMessageInterface } from "../../Connexion/ConnexionModels"; -import {hasMovedEventName, Player, requestEmoteEventName} from "../Player/Player"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { Room } from "../../Connexion/Room"; +import type { RoomConnection } from "../../Connexion/RoomConnection"; +import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, - POSITION_DELAY, + POSITION_DELAY } from "../../Enum/EnvironmentVariable"; +import { TextureError } from "../../Exception/TextureError"; +import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; +import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; +import { peerStore } from "../../Stores/PeerStore"; +import { touchScreenManager } from "../../Touch/TouchScreenManager"; +import { urlManager } from "../../Url/UrlManager"; +import { audioManager } from "../../WebRtc/AudioManager"; +import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { HtmlUtils } from "../../WebRtc/HtmlUtils"; +import { jitsiFactory } from "../../WebRtc/JitsiFactory"; +import { + AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, CenterListener, + JITSI_MESSAGE_PROPERTIES, + layoutManager, + LayoutMode, + ON_ACTION_TRIGGER_BUTTON, + TRIGGER_JITSI_PROPERTIES, + TRIGGER_WEBSITE_PROPERTIES, + WEBSITE_MESSAGE_PROPERTIES +} from "../../WebRtc/LayoutManager"; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer"; +import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import { ChatModeIcon } from "../Components/ChatModeIcon"; +import { addLoader } from "../Components/Loader"; +import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; +import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; +import { PresentationModeIcon } from "../Components/PresentationModeIcon"; +import { TextUtils } from "../Components/TextUtils"; +import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; +import { RemotePlayer } from "../Entity/RemotePlayer"; +import type { ActionableItem } from "../Items/ActionableItem"; +import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; +import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; import type { ITiledMap, ITiledMapLayer, @@ -24,80 +67,35 @@ import type { ITiledMapTileLayer, ITiledTileSet } from "../Map/ITiledMap"; -import type {AddPlayerInterface} from "./AddPlayerInterface"; -import {PlayerAnimationDirections} from "../Player/Animation"; -import {PlayerMovement} from "./PlayerMovement"; -import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; -import {RemotePlayer} from "../Entity/RemotePlayer"; -import {Queue} from 'queue-typescript'; -import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; -import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import {lazyLoadPlayerCharacterTextures, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; -import { - CenterListener, - JITSI_MESSAGE_PROPERTIES, - layoutManager, - LayoutMode, - ON_ACTION_TRIGGER_BUTTON, - TRIGGER_JITSI_PROPERTIES, - TRIGGER_WEBSITE_PROPERTIES, - WEBSITE_MESSAGE_PROPERTIES, - AUDIO_VOLUME_PROPERTY, - AUDIO_LOOP_PROPERTY -} from "../../WebRtc/LayoutManager"; -import {GameMap} from "./GameMap"; -import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; -import {mediaManager} from "../../WebRtc/MediaManager"; -import type {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; -import type {ActionableItem} from "../Items/ActionableItem"; -import {UserInputManager} from "../UserInput/UserInputManager"; -import {soundManager} from "./SoundManager"; -import type {UserMovedMessage} from "../../Messages/generated/messages_pb"; -import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; -import {connectionManager} from "../../Connexion/ConnectionManager"; -import type {RoomConnection} from "../../Connexion/RoomConnection"; -import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; -import {userMessageManager} from "../../Administration/UserMessageManager"; -import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; -import {ResizableScene} from "../Login/ResizableScene"; -import {Room} from "../../Connexion/Room"; -import {jitsiFactory} from "../../WebRtc/JitsiFactory"; -import {urlManager} from "../../Url/UrlManager"; -import {audioManager} from "../../WebRtc/AudioManager"; -import {PresentationModeIcon} from "../Components/PresentationModeIcon"; -import {ChatModeIcon} from "../Components/ChatModeIcon"; -import {OpenChatIcon, openChatIconName} from "../Components/OpenChatIcon"; -import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; -import {TextureError} from "../../Exception/TextureError"; -import {addLoader} from "../Components/Loader"; -import {ErrorSceneName} from "../Reconnecting/ErrorScene"; -import {localUserStore} from "../../Connexion/LocalUserStore"; -import {iframeListener} from "../../Api/IframeListener"; -import {HtmlUtils} from "../../WebRtc/HtmlUtils"; +import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; +import { PlayerAnimationDirections } from "../Player/Animation"; +import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; +import { ErrorSceneName } from "../Reconnecting/ErrorScene"; +import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; +import { waScaleManager } from "../Services/WaScaleManager"; +import { PinchManager } from "../UserInput/PinchManager"; +import { UserInputManager } from "../UserInput/UserInputManager"; +import type { AddPlayerInterface } from "./AddPlayerInterface"; +import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; +import { DirtyScene } from "./DirtyScene"; +import { EmoteManager } from "./EmoteManager"; +import { gameManager, HasMovedEvent } from "./GameManager"; +import { GameMap } from "./GameMap"; +import { PlayerMovement } from "./PlayerMovement"; +import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; +import { soundManager } from "./SoundManager"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; import GameObject = Phaser.GameObjects.GameObject; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import DOMElement = Phaser.GameObjects.DOMElement; -import EVENT_TYPE =Phaser.Scenes.Events -import type {Subscription} from "rxjs"; -import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; -import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import EVENT_TYPE = Phaser.Scenes.Events import RenderTexture = Phaser.GameObjects.RenderTexture; import Tilemap = Phaser.Tilemaps.Tilemap; -import {DirtyScene} from "./DirtyScene"; -import {TextUtils} from "../Components/TextUtils"; -import {touchScreenManager} from "../../Touch/TouchScreenManager"; -import {PinchManager} from "../UserInput/PinchManager"; -import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; -import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes"; -import {waScaleManager} from "../Services/WaScaleManager"; -import {peerStore} from "../../Stores/PeerStore"; -import {EmoteManager} from "./EmoteManager"; export interface GameSceneInitInterface { - initPosition: PointInterface|null, + initPosition: PointInterface | null, reconnecting: boolean } @@ -134,10 +132,10 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; export class GameScene extends DirtyScene implements CenterListener { - Terrains : Array; + Terrains: Array; CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; - MapPlayersByKey : Map = new Map(); + MapPlayersByKey: Map = new Map(); Map!: Phaser.Tilemaps.Tilemap; Layers!: Array; Objects!: Array; @@ -147,10 +145,10 @@ export class GameScene extends DirtyScene implements CenterListener { startY!: number; circleTexture!: CanvasTexture; circleRedTexture!: CanvasTexture; - pendingEvents: Queue = new Queue(); - private initPosition: PositionInterface|null = null; + pendingEvents: Queue = new Queue(); + private initPosition: PositionInterface | null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); - public connection: RoomConnection|undefined; + public connection: RoomConnection | undefined; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; @@ -159,7 +157,7 @@ export class GameScene extends DirtyScene implements CenterListener { // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; - private iframeSubscriptionList! : Array; + private iframeSubscriptionList!: Array; private peerStoreUnsubscribe!: () => void; MapUrlFile: string; RoomId: string; @@ -179,22 +177,22 @@ export class GameScene extends DirtyScene implements CenterListener { private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. - private outlinedItem: ActionableItem|null = null; + private outlinedItem: ActionableItem | null = null; public userInputManager!: UserInputManager; - private isReconnecting: boolean|undefined = undefined; + private isReconnecting: boolean | undefined = undefined; private startLayerName!: string | null; private openChatIcon!: OpenChatIcon; private playerName!: string; private characterLayers!: string[]; - private companion!: string|null; - private messageSubscription: Subscription|null = null; - private popUpElements : Map = new Map(); - private originalMapUrl: string|undefined; - private pinchManager: PinchManager|undefined; + private companion!: string | null; + private messageSubscription: Subscription | null = null; + private popUpElements: Map = new Map(); + private originalMapUrl: string | undefined; + private pinchManager: PinchManager | undefined; private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private emoteManager!: EmoteManager; - constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { + constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ key: customKey ?? room.id }); @@ -234,13 +232,13 @@ export class GameScene extends DirtyScene implements CenterListener { //this.load.audio('audio-report-message', '/resources/objects/report-message.mp3'); this.sound.pauseOnBlur = false; - this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { + this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { this.originalMapUrl = this.MapUrlFile; this.MapUrlFile = this.MapUrlFile.replace('http://', 'https://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); return; @@ -254,7 +252,7 @@ export class GameScene extends DirtyScene implements CenterListener { this.originalMapUrl = this.MapUrlFile; this.MapUrlFile = this.MapUrlFile.replace('https://', 'http://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); return; @@ -266,7 +264,7 @@ export class GameScene extends DirtyScene implements CenterListener { message: this.originalMapUrl ?? file.src }); }); - this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (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 @@ -278,7 +276,7 @@ export class GameScene extends DirtyScene implements CenterListener { this.onMapLoad(data); } - this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32}); + this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', { frameWidth: 32, frameHeight: 32 }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); //eslint-disable-next-line @typescript-eslint/no-explicit-any (this.load as any).rexWebFont({ @@ -315,7 +313,7 @@ export class GameScene extends DirtyScene implements CenterListener { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup') { for (const object of layer.objects) { - let objectsOfType: ITiledMapObject[]|undefined; + let objectsOfType: ITiledMapObject[] | undefined; if (!objects.has(object.type)) { objectsOfType = new Array(); } else { @@ -343,7 +341,7 @@ export class GameScene extends DirtyScene implements CenterListener { } default: continue; - //throw new Error('Unsupported object type: "'+ itemType +'"'); + //throw new Error('Unsupported object type: "'+ itemType +'"'); } itemFactory.preload(this.load); @@ -378,7 +376,7 @@ export class GameScene extends DirtyScene implements CenterListener { } //hook initialisation - init(initData : GameSceneInitInterface) { + init(initData: GameSceneInitInterface) { if (initData.initPosition !== undefined) { this.initPosition = initData.initPosition; //todo: still used? } @@ -457,7 +455,7 @@ export class GameScene extends DirtyScene implements CenterListener { this.Objects = new Array(); //initialise list of other player - this.MapPlayers = this.physics.add.group({immovable: true}); + this.MapPlayers = this.physics.add.group({ immovable: true }); //create input to move @@ -563,7 +561,7 @@ export class GameScene extends DirtyScene implements CenterListener { bottom: camera.scrollY + camera.height, }, this.companion - ).then((onConnect: OnConnectInterface) => { + ).then((onConnect: OnConnectInterface) => { this.connection = onConnect.connection; this.connection.onUserJoins((message: MessageUserJoined) => { @@ -716,23 +714,23 @@ export class GameScene extends DirtyScene implements CenterListener { const contextRed = this.circleRedTexture.context; contextRed.beginPath(); contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); - //context.lineWidth = 5; + //context.lineWidth = 5; contextRed.strokeStyle = '#ff0000'; contextRed.stroke(); this.circleRedTexture.refresh(); } - private safeParseJSONstring(jsonString: string|undefined, propertyName: string) { + private safeParseJSONstring(jsonString: string | undefined, propertyName: string) { try { return jsonString ? JSON.parse(jsonString) : {}; - } catch(e) { + } catch (e) { console.warn('Invalid JSON found in property "' + propertyName + '" of the map:' + jsonString, e); return {} } } - private triggerOnMapLayerPropertyChange(){ + private triggerOnMapLayerPropertyChange() { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); @@ -743,22 +741,22 @@ export class GameScene extends DirtyScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('openWebsite', this.userInputManager); coWebsiteManager.closeCoWebsite(); - }else{ + } else { const openWebsiteFunction = () => { coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined); layoutManager.removeActionButton('openWebsite', this.userInputManager); }; const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); - if(openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); - if(message === undefined){ + if (message === undefined) { message = 'Press SPACE or touch here to open web site'; } layoutManager.addActionButton('openWebsite', message.toString(), () => { openWebsiteFunction(); }, this.userInputManager); - }else{ + } else { openWebsiteFunction(); } } @@ -767,12 +765,12 @@ export class GameScene extends DirtyScene implements CenterListener { if (newValue === undefined) { layoutManager.removeActionButton('jitsiRoom', this.userInputManager); this.stopJitsi(); - }else{ + } else { const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); - const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; + const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; if (JITSI_PRIVATE_MODE && !jitsiUrl) { - const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; + const adminTag = allProps.get("jitsiRoomAdminTag") as string | undefined; this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); } else { @@ -782,7 +780,7 @@ export class GameScene extends DirtyScene implements CenterListener { } const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); - if(jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(JITSI_MESSAGE_PROPERTIES); if (message === undefined) { message = 'Press SPACE or touch here to enter Jitsi Meet room'; @@ -790,7 +788,7 @@ export class GameScene extends DirtyScene implements CenterListener { layoutManager.addActionButton('jitsiRoom', message.toString(), () => { openJitsiRoomFunction(); }, this.userInputManager); - }else{ + } else { openJitsiRoomFunction(); } } @@ -803,8 +801,8 @@ export class GameScene extends DirtyScene implements CenterListener { } }); this.gameMap.onPropertyChange('playAudio', (newValue, oldValue, allProps) => { - const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number|undefined; - const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean|undefined; + const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined; + const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined; newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop); }); // TODO: This legacy property should be removed at some point @@ -823,13 +821,13 @@ export class GameScene extends DirtyScene implements CenterListener { } private listenToIframeEvents(): void { - this.iframeSubscriptionList = []; - this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { + this.iframeSubscriptionList = []; + this.iframeSubscriptionList.push(iframeListener.openPopupStream.subscribe((openPopupEvent) => { - let objectLayerSquare : ITiledMapObject; + let objectLayerSquare: ITiledMapObject; const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject); - if (targetObjectData !== undefined){ - objectLayerSquare = targetObjectData; + if (targetObjectData !== undefined) { + objectLayerSquare = targetObjectData; } else { console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map."); return; @@ -842,14 +840,14 @@ ${escapedMessage} html += buttonContainer; let id = 0; for (const button of openPopupEvent.buttons) { - html += ``; + html += ``; id++; } html += '
'; - const domElement = this.add.dom(objectLayerSquare.x , + const domElement = this.add.dom(objectLayerSquare.x, objectLayerSquare.y).createFromHTML(html); - const container : HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement; + const container: HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement; container.style.width = objectLayerSquare.width + "px"; domElement.scale = 0; domElement.setClassName('popUpElement'); @@ -869,73 +867,70 @@ ${escapedMessage} id++; } this.tweens.add({ - targets : domElement , - scale : 1, - ease : "EaseOut", - duration : 400, + targets: domElement, + scale: 1, + ease: "EaseOut", + duration: 400, }); this.popUpElements.set(openPopupEvent.popupId, domElement); })); - this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { + this.iframeSubscriptionList.push(iframeListener.closePopupStream.subscribe((closePopupEvent) => { const popUpElement = this.popUpElements.get(closePopupEvent.popupId); if (popUpElement === undefined) { - console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?'); + console.error('Could not close popup with ID ', closePopupEvent.popupId, '. Maybe it has already been closed?'); } this.tweens.add({ - targets : popUpElement , - scale : 0, - ease : "EaseOut", - duration : 400, - onComplete : () => { + targets: popUpElement, + scale: 0, + ease: "EaseOut", + duration: 400, + onComplete: () => { popUpElement?.destroy(); this.popUpElements.delete(closePopupEvent.popupId); }, }); })); - this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.disablePlayerControlStream.subscribe(() => { this.userInputManager.disableControls(); })); - this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent)=> - { - const url = new URL(playSoundEvent.url, this.MapUrlFile); - soundManager.playSound(this.load,this.sound,url.toString(),playSoundEvent.config); - })) + this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent) => { + const url = new URL(playSoundEvent.url, this.MapUrlFile); + soundManager.playSound(this.load, this.sound, url.toString(), playSoundEvent.config); + })) - this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent)=> - { + this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent) => { const url = new URL(stopSoundEvent.url, this.MapUrlFile); - soundManager.stopSound(this.sound,url.toString()); + soundManager.stopSound(this.sound, url.toString()); })) - this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent)=> - { + this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent) => { const url = new URL(loadSoundEvent.url, this.MapUrlFile); - soundManager.loadSound(this.load,this.sound,url.toString()); + soundManager.loadSound(this.load, this.sound, url.toString()); })) - this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(() => { this.userInputManager.restoreControls(); })); - this.iframeSubscriptionList.push(iframeListener.loadPageStream.subscribe((url:string)=>{ - this.loadNextGame(url).then(()=>{ - this.events.once(EVENT_TYPE.POST_UPDATE,()=>{ + this.iframeSubscriptionList.push(iframeListener.loadPageStream.subscribe((url: string) => { + this.loadNextGame(url).then(() => { + this.events.once(EVENT_TYPE.POST_UPDATE, () => { this.onMapExit(url); }) }) })); - let scriptedBubbleSprite : Sprite; - this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(()=>{ - scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white'); + let scriptedBubbleSprite: Sprite; + this.iframeSubscriptionList.push(iframeListener.displayBubbleStream.subscribe(() => { + scriptedBubbleSprite = new Sprite(this, this.CurrentPlayer.x + 25, this.CurrentPlayer.y, 'circleSprite-white'); scriptedBubbleSprite.setDisplayOrigin(48, 48); this.add.existing(scriptedBubbleSprite); })); - this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(()=>{ + this.iframeSubscriptionList.push(iframeListener.removeBubbleStream.subscribe(() => { scriptedBubbleSprite.destroy(); })); @@ -948,9 +943,11 @@ ${escapedMessage} private onMapExit(exitKey: string) { if (this.mapTransitioning) return; this.mapTransitioning = true; - const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); - if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); + const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance); + if (!roomId) throw new Error('Could not find the room from its exit key: ' + exitKey); urlManager.pushStartLayerNameToUrl(hash); + const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene + menuScene.reset() if (roomId !== this.scene.key) { if (this.scene.get(roomId) === null) { console.error("next room not loaded", exitKey); @@ -992,7 +989,7 @@ ${escapedMessage} mediaManager.hideGameOverlay(); - for(const iframeEvents of this.iframeSubscriptionList){ + for (const iframeEvents of this.iframeSubscriptionList) { iframeEvents.unsubscribe(); } } @@ -1012,7 +1009,7 @@ ${escapedMessage} private switchLayoutMode(): void { //if discussion is activated, this layout cannot be activated - if(mediaManager.activatedDiscussion){ + if (mediaManager.activatedDiscussion) { return; } const mode = layoutManager.getLayoutMode(); @@ -1053,24 +1050,24 @@ ${escapedMessage} private initPositionFromLayerName(layerName: string) { for (const layer of this.gameMap.layersIterator) { - if ((layerName === layer.name || layer.name.endsWith('/'+layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { + if ((layerName === layer.name || layer.name.endsWith('/' + layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { const startPosition = this.startUser(layer); - this.startX = startPosition.x + this.mapFile.tilewidth/2; - this.startY = startPosition.y + this.mapFile.tileheight/2; + this.startX = startPosition.x + this.mapFile.tilewidth / 2; + this.startY = startPosition.y + this.mapFile.tileheight / 2; } } } - private getExitUrl(layer: ITiledMapLayer): string|undefined { - return this.getProperty(layer, "exitUrl") as string|undefined; + private getExitUrl(layer: ITiledMapLayer): string | undefined { + 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 getExitSceneUrl(layer: ITiledMapLayer): string | undefined { + return this.getProperty(layer, "exitSceneUrl") as string | undefined; } private isStartLayer(layer: ITiledMapLayer): boolean { @@ -1081,8 +1078,8 @@ ${escapedMessage} return (this.getProperties(map, "script") as string[]).map((script) => (new URL(script, this.MapUrlFile)).toString()); } - private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined { - const properties: ITiledMapLayerProperty[]|undefined = layer.properties; + private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined { + const properties: ITiledMapLayerProperty[] | undefined = layer.properties; if (!properties) { return undefined; } @@ -1093,8 +1090,8 @@ ${escapedMessage} return obj.value; } - private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { - const properties: ITiledMapLayerProperty[]|undefined = layer.properties; + private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] { + const properties: ITiledMapLayerProperty[] | undefined = layer.properties; if (!properties) { return []; } @@ -1103,29 +1100,29 @@ ${escapedMessage} //todo: push that into the gameManager private loadNextGame(exitSceneIdentifier: string): Promise { - const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); + const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const room = new Room(roomId); - return gameManager.loadMap(room, this.scene).catch(() => {}); + return gameManager.loadMap(room, this.scene).catch(() => { }); } private startUser(layer: ITiledMapTileLayer): PositionInterface { const tiles = layer.data; - if (typeof(tiles) === 'string') { + if (typeof (tiles) === 'string') { throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); } - const possibleStartPositions : PositionInterface[] = []; - tiles.forEach((objectKey : number, key: number) => { - if(objectKey === 0){ + const possibleStartPositions: PositionInterface[] = []; + tiles.forEach((objectKey: number, key: number) => { + if (objectKey === 0) { return; } const y = Math.floor(key / layer.width); const x = key % layer.width; - possibleStartPositions.push({x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth}); + possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth }); }); // Get a value at random amongst allowed values if (possibleStartPositions.length === 0) { - console.warn('The start layer "'+layer.name+'" for this map is empty.'); + console.warn('The start layer "' + layer.name + '" for this map is empty.'); return { x: 0, y: 0 @@ -1137,12 +1134,12 @@ ${escapedMessage} //todo: in a dedicated class/function? initCamera() { - this.cameras.main.setBounds(0,0, this.Map.widthInPixels, this.Map.heightInPixels); + this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.cameras.main.startFollow(this.CurrentPlayer, true); this.updateCameraOffset(); } - addLayer(Layer : Phaser.Tilemaps.TilemapLayer){ + addLayer(Layer: Phaser.Tilemaps.TilemapLayer) { this.Layers.push(Layer); } @@ -1152,7 +1149,7 @@ ${escapedMessage} this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - Layer.setCollisionByProperty({collides: true}); + Layer.setCollisionByProperty({ collides: true }); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer Layer.renderDebug(this.add.graphics(), { @@ -1164,7 +1161,7 @@ ${escapedMessage} }); } - createCurrentPlayer(){ + createCurrentPlayer() { //TODO create animation moving between exit and start const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers); try { @@ -1189,8 +1186,8 @@ ${escapedMessage} this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => { this.connection?.emitEmoteEvent(emoteKey); }) - }catch (err){ - if(err instanceof TextureError) { + } catch (err) { + if (err instanceof TextureError) { gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); } throw err; @@ -1251,7 +1248,7 @@ ${escapedMessage} } let shortestDistance: number = Infinity; - let selectedItem: ActionableItem|null = null; + let selectedItem: ActionableItem | null = null; for (const item of this.actionableItems.values()) { const distance = item.actionableDistance(x, y); if (distance !== null && distance < shortestDistance) { @@ -1285,7 +1282,7 @@ ${escapedMessage} * @param time * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ - update(time: number, delta: number) : void { + update(time: number, delta: number): void { this.dirty = false; mediaManager.updateScene(); this.currentTick = time; @@ -1345,8 +1342,8 @@ ${escapedMessage} const currentPlayerId = this.connection?.getUserId(); this.removeAllRemotePlayers(); // load map - usersPosition.forEach((userPosition : MessageUserPositionInterface) => { - if(userPosition.userId === currentPlayerId){ + usersPosition.forEach((userPosition: MessageUserPositionInterface) => { + if (userPosition.userId === currentPlayerId) { return; } this.addPlayer(userPosition); @@ -1356,16 +1353,16 @@ ${escapedMessage} /** * Called by the connexion when a new player arrives on a map */ - public addPlayer(addPlayerData : AddPlayerInterface) : void { + public addPlayer(addPlayerData: AddPlayerInterface): void { this.pendingEvents.enqueue({ type: "AddPlayerEvent", event: addPlayerData }); } - private doAddPlayer(addPlayerData : AddPlayerInterface): void { + private doAddPlayer(addPlayerData: AddPlayerInterface): void { //check if exist player, if exist, move position - if(this.MapPlayersByKey.has(addPlayerData.userId)){ + if (this.MapPlayersByKey.has(addPlayerData.userId)) { this.updatePlayerPosition({ userId: addPlayerData.userId, position: addPlayerData.position @@ -1427,10 +1424,10 @@ ${escapedMessage} } private doUpdatePlayerPosition(message: MessageUserMovedInterface): void { - const player : RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); + const player: RemotePlayer | undefined = this.MapPlayersByKey.get(message.userId); if (player === undefined) { //throw new Error('Cannot find player with ID "' + message.userId +'"'); - console.error('Cannot update position of player with ID "' + message.userId +'": player not found'); + console.error('Cannot update position of player with ID "' + message.userId + '": player not found'); return; } @@ -1474,7 +1471,7 @@ ${escapedMessage} doDeleteGroup(groupId: number): void { const group = this.groups.get(groupId); - if(!group){ + if (!group) { return; } group.destroy(); @@ -1503,7 +1500,7 @@ ${escapedMessage} bottom: camera.scrollY + camera.height, }); } - private getObjectLayerData(objectName : string) : ITiledMapObject| undefined{ + private getObjectLayerData(objectName: string): ITiledMapObject | undefined { for (const layer of this.mapFile.layers) { if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { for (const object of layer.objects) { @@ -1537,7 +1534,7 @@ ${escapedMessage} const game = HtmlUtils.querySelectorOrFail('#game canvas'); // Let's put this in Game coordinates by applying the zoom level: - this.cameras.main.setFollowOffset((xCenter - game.offsetWidth/2) * window.devicePixelRatio / this.scale.zoom , (yCenter - game.offsetHeight/2) * window.devicePixelRatio / this.scale.zoom); + this.cameras.main.setFollowOffset((xCenter - game.offsetWidth / 2) * window.devicePixelRatio / this.scale.zoom, (yCenter - game.offsetHeight / 2) * window.devicePixelRatio / this.scale.zoom); } public onCenterChange(): void { @@ -1546,16 +1543,16 @@ ${escapedMessage} public startJitsi(roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); - const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string|undefined, 'jitsiConfig'); - const jitsiInterfaceConfig = this.safeParseJSONstring(allProps.get("jitsiInterfaceConfig") as string|undefined, 'jitsiInterfaceConfig'); - const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; + const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string | undefined, 'jitsiConfig'); + const jitsiInterfaceConfig = this.safeParseJSONstring(allProps.get("jitsiInterfaceConfig") as string | undefined, 'jitsiInterfaceConfig'); + const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl); this.connection?.setSilent(true); mediaManager.hideGameOverlay(); //permit to stop jitsi when user close iframe - mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => { + mediaManager.addTriggerCloseJitsiFrameButton('close-jisi', () => { this.stopJitsi(); }); } @@ -1569,7 +1566,7 @@ ${escapedMessage} } //todo: put this into an 'orchestrator' scene (EntryScene?) - private bannedUser(){ + private bannedUser() { this.cleanupClosingScene(); this.userInputManager.disableControls(); this.scene.start(ErrorSceneName, { @@ -1580,22 +1577,22 @@ ${escapedMessage} } //todo: put this into an 'orchestrator' scene (EntryScene?) - private showWorldFullError(message: string|null): void { + private showWorldFullError(message: string | null): void { this.cleanupClosingScene(); this.scene.stop(ReconnectingSceneName); this.scene.remove(ReconnectingSceneName); this.userInputManager.disableControls(); //FIX ME to use status code - if(message == undefined){ + if (message == undefined) { this.scene.start(ErrorSceneName, { title: 'Connection rejected', subTitle: 'The world you are trying to join is full. Try again later.', message: 'If you want more information, you may contact us at: workadventure@thecodingmachine.com' }); - }else{ + } else { this.scene.start(ErrorSceneName, { title: 'Connection rejected', - subTitle: 'You cannot join the World. Try again later. \n\r \n\r Error: '+message+'.', + subTitle: 'You cannot join the World. Try again later. \n\r \n\r Error: ' + message + '.', message: 'If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com' }); } diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 54fa395a..d5d0387a 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -1,16 +1,20 @@ -import {LoginScene, LoginSceneName} from "../Login/LoginScene"; -import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; -import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene"; -import {gameManager} from "../Game/GameManager"; -import {localUserStore} from "../../Connexion/LocalUserStore"; -import {mediaManager} from "../../WebRtc/MediaManager"; -import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu"; -import {connectionManager} from "../../Connexion/ConnectionManager"; -import {GameConnexionTypes} from "../../Url/UrlManager"; -import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; -import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; -import {menuIconVisible} from "../../Stores/MenuStore"; -import {videoConstraintStore} from "../../Stores/MediaStore"; +import { Subscription } from 'rxjs'; +import { sendMenuClickedEvent } from '../../Api/Events/ui/MenuItemClickedEvent'; +import { registerMenuCommandStream } from '../../Api/Events/ui/MenuItemRegisterEvent'; +import { connectionManager } from "../../Connexion/ConnectionManager"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream"; +import { videoConstraintStore } from "../../Stores/MediaStore"; +import { menuIconVisible } from "../../Stores/MenuStore"; +import { GameConnexionTypes } from "../../Url/UrlManager"; +import { HtmlUtils } from '../../WebRtc/HtmlUtils'; +import { mediaManager } from "../../WebRtc/MediaManager"; +import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer"; +import { gameManager } from "../Game/GameManager"; +import { LoginScene, LoginSceneName } from "../Login/LoginScene"; +import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; +import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene"; +import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu"; export const MenuSceneName = 'MenuScene'; const gameMenuKey = 'gameMenu'; @@ -37,15 +41,38 @@ export class MenuScene extends Phaser.Scene { private menuButton!: Phaser.GameObjects.DOMElement; private warningContainer: WarningContainer | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null; - + private subscriptions = new Subscription() constructor() { - super({key: MenuSceneName}); + super({ key: MenuSceneName }); this.gameQualityValue = localUserStore.getGameQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue(); + + this.subscriptions.add(registerMenuCommandStream.subscribe(menuCommand => { + this.addMenuOption(menuCommand); + + })) } - preload () { + reset() { + const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")]; + for (let index = addedMenuItems.length - 1; index >= 0; index--) { + addedMenuItems[index].remove() + } + } + + public addMenuOption(menuText: string) { + const wrappingSection = document.createElement("section") + const escapedHtml = HtmlUtils.escapeHtml(menuText); + wrappingSection.innerHTML = `` + const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); + if (menuItemContainer) { + menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove() + menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) + } + } + + preload() { this.load.html(gameMenuKey, 'resources/html/gameMenu.html'); this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); @@ -68,11 +95,11 @@ export class MenuScene extends Phaser.Scene { this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare); MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare); this.gameShareElement.addListener('click'); - this.gameShareElement.on('click', (event:MouseEvent) => { + this.gameShareElement.on('click', (event: MouseEvent) => { event.preventDefault(); - if((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') { + if ((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') { this.copyLink(); - }else if((event?.target as HTMLInputElement).id === 'gameShareFormCancel') { + } else if ((event?.target as HTMLInputElement).id === 'gameShareFormCancel') { this.closeGameShare(); } }); @@ -128,8 +155,8 @@ export class MenuScene extends Phaser.Scene { } //TODO bind with future metadata of card //if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){ - const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement; - adminSection.hidden = false; + const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement; + adminSection.hidden = false; //} this.tweens.add({ targets: this.menuElement, @@ -179,28 +206,28 @@ export class MenuScene extends Phaser.Scene { this.settingsMenuOpened = true; const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; - gameQualitySelect.value = ''+this.gameQualityValue; + gameQualitySelect.value = '' + this.gameQualityValue; const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; - videoQualitySelect.value = ''+this.videoQualityValue; + videoQualitySelect.value = '' + this.videoQualityValue; this.gameQualityMenuElement.addListener('click'); - this.gameQualityMenuElement.on('click', (event:MouseEvent) => { + this.gameQualityMenuElement.on('click', (event: MouseEvent) => { event.preventDefault(); if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') { const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value)); - } else if((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') { + } else if ((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') { this.closeGameQualityMenu(); } }); - let middleY = this.scale.height / 2 - 392/2; - if(middleY < 0){ + let middleY = this.scale.height / 2 - 392 / 2; + if (middleY < 0) { middleY = 0; } - let middleX = this.scale.width / 2 - 457/2; - if(middleX < 0){ + let middleX = this.scale.width / 2 - 457 / 2; + if (middleX < 0) { middleX = 0; } this.tweens.add({ @@ -226,7 +253,7 @@ export class MenuScene extends Phaser.Scene { } - private openGameShare(): void{ + private openGameShare(): void { if (this.gameShareOpened) { this.closeGameShare(); return; @@ -240,11 +267,11 @@ export class MenuScene extends Phaser.Scene { this.gameShareOpened = true; let middleY = this.scale.height / 2 - 85; - if(middleY < 0){ + if (middleY < 0) { middleY = 0; } let middleX = this.scale.width / 2 - 200; - if(middleX < 0){ + if (middleX < 0) { middleX = 0; } this.tweens.add({ @@ -256,7 +283,7 @@ export class MenuScene extends Phaser.Scene { }); } - private closeGameShare(): void{ + private closeGameShare(): void { const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement; gameShareInfo.innerText = ''; gameShareInfo.style.display = 'none'; @@ -269,12 +296,18 @@ export class MenuScene extends Phaser.Scene { }); } - private onMenuClick(event:MouseEvent) { - if((event?.target as HTMLInputElement).classList.contains('not-button')){ + private onMenuClick(event: MouseEvent) { + const htmlMenuItem = (event?.target as HTMLInputElement); + if (htmlMenuItem.classList.contains('not-button')) { return; } event.preventDefault(); + if (htmlMenuItem.classList.contains("fromApi")) { + sendMenuClickedEvent(htmlMenuItem.id) + return + } + switch ((event?.target as HTMLInputElement).id) { case 'changeNameButton': this.closeSideMenu(); @@ -316,7 +349,7 @@ export class MenuScene extends Phaser.Scene { gameShareInfo.style.display = 'block'; } - private saveSetting(valueGame: number, valueVideo: number){ + private saveSetting(valueGame: number, valueVideo: number) { if (valueGame !== this.gameQualityValue) { this.gameQualityValue = valueGame; localUserStore.setGameQualityValue(valueGame); @@ -337,7 +370,7 @@ export class MenuScene extends Phaser.Scene { window.open(sparkHost, '_blank'); } - private closeAll(){ + private closeAll() { this.closeGameQualityMenu(); this.closeGameShare(); this.gameReportElement.close(); From 64a00481f031699b3a5d9ef37fcd038294a80737 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 21 Jun 2021 18:39:02 +0200 Subject: [PATCH 52/87] fixed wrong import --- .../src/Api/Events/ui/MenuItemClickedEvent.ts | 9 -------- front/src/Api/iframe/Ui/MenuItem.ts | 11 +++++++++ front/src/Phaser/Menu/MenuScene.ts | 2 +- front/src/iframe_api.ts | 23 ++++++++++--------- 4 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 front/src/Api/iframe/Ui/MenuItem.ts diff --git a/front/src/Api/Events/ui/MenuItemClickedEvent.ts b/front/src/Api/Events/ui/MenuItemClickedEvent.ts index 6444cb09..fad2944f 100644 --- a/front/src/Api/Events/ui/MenuItemClickedEvent.ts +++ b/front/src/Api/Events/ui/MenuItemClickedEvent.ts @@ -1,5 +1,4 @@ import * as tg from "generic-type-guard"; -import { iframeListener } from '../../IframeListener'; export const isMenuItemClickedEvent = new tg.IsInterface().withProperties({ @@ -11,11 +10,3 @@ export const isMenuItemClickedEvent = export type MenuItemClickedEvent = tg.GuardedType; -export function sendMenuClickedEvent(menuItem: string) { - iframeListener.postMessage({ - 'type': 'menuItemClicked', - 'data': { - menuItem: menuItem, - } as MenuItemClickedEvent - }); -} \ No newline at end of file diff --git a/front/src/Api/iframe/Ui/MenuItem.ts b/front/src/Api/iframe/Ui/MenuItem.ts new file mode 100644 index 00000000..9782ea7a --- /dev/null +++ b/front/src/Api/iframe/Ui/MenuItem.ts @@ -0,0 +1,11 @@ +import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent'; +import { iframeListener } from '../../IframeListener'; + +export function sendMenuClickedEvent(menuItem: string) { + iframeListener.postMessage({ + 'type': 'menuItemClicked', + 'data': { + menuItem: menuItem, + } as MenuItemClickedEvent + }); +} \ No newline at end of file diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index d5d0387a..e405a758 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -1,6 +1,6 @@ import { Subscription } from 'rxjs'; -import { sendMenuClickedEvent } from '../../Api/Events/ui/MenuItemClickedEvent'; import { registerMenuCommandStream } from '../../Api/Events/ui/MenuItemRegisterEvent'; +import { sendMenuClickedEvent } from '../../Api/iframe/Ui/MenuItem'; import { connectionManager } from "../../Connexion/ConnectionManager"; import { localUserStore } from "../../Connexion/LocalUserStore"; import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream"; diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index ae5321cf..874f0ace 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -1,4 +1,3 @@ -import {registeredCallbacks} from "./Api/iframe/registeredCallbacks"; import { IframeResponseEvent, IframeResponseEventMap, @@ -6,15 +5,17 @@ import { TypedMessageEvent } from "./Api/Events/IframeEvent"; import chat from "./Api/iframe/chat"; -import type {IframeCallback} from './Api/iframe/IframeApiContribution'; -import nav from "./Api/iframe/nav"; import controls from "./Api/iframe/controls"; -import ui from "./Api/iframe/ui"; -import sound from "./Api/iframe/sound"; +import type { IframeCallback } from './Api/iframe/IframeApiContribution'; +import nav from "./Api/iframe/nav"; +import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; import room from "./Api/iframe/room"; -import type {ButtonDescriptor} from "./Api/iframe/Ui/ButtonDescriptor"; -import type {Popup} from "./Api/iframe/Ui/Popup"; -import type {Sound} from "./Api/iframe/Sound/Sound"; +import sound from "./Api/iframe/sound"; +import type { Sound } from "./Api/iframe/Sound/Sound"; +import ui from "./Api/iframe/ui"; +import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; +import type { Popup } from "./Api/iframe/Ui/Popup"; + const wa = { ui, @@ -77,7 +78,7 @@ const wa = { /** * @deprecated Use WA.sound.loadSound instead */ - loadSound(url: string) : Sound { + loadSound(url: string): Sound { console.warn('Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead'); return sound.loadSound(url); }, @@ -85,7 +86,7 @@ const wa = { /** * @deprecated Use WA.nav.goToPage instead */ - goToPage(url : string) : void { + goToPage(url: string): void { console.warn('Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead'); nav.goToPage(url); }, @@ -101,7 +102,7 @@ const wa = { /** * @deprecated Use WA.nav.openCoWebSite instead */ - openCoWebSite(url : string) : void{ + openCoWebSite(url: string): void { console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead'); nav.openCoWebSite(url); }, From 8be29062f6c17bcd7eb8c5b225944b0b345dd4e4 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 21 Jun 2021 18:41:41 +0200 Subject: [PATCH 53/87] reverted import sorting --- front/src/iframe_api.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 874f0ace..7b6b2db9 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -1,3 +1,4 @@ +import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; import { IframeResponseEvent, IframeResponseEventMap, @@ -5,17 +6,15 @@ import { TypedMessageEvent } from "./Api/Events/IframeEvent"; import chat from "./Api/iframe/chat"; -import controls from "./Api/iframe/controls"; import type { IframeCallback } from './Api/iframe/IframeApiContribution'; import nav from "./Api/iframe/nav"; -import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; -import room from "./Api/iframe/room"; -import sound from "./Api/iframe/sound"; -import type { Sound } from "./Api/iframe/Sound/Sound"; +import controls from "./Api/iframe/controls"; import ui from "./Api/iframe/ui"; +import sound from "./Api/iframe/sound"; +import room from "./Api/iframe/room"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { Popup } from "./Api/iframe/Ui/Popup"; - +import type { Sound } from "./Api/iframe/Sound/Sound"; const wa = { ui, From 4334ae8c9ddc2c8b0c05bb8d1dc9e77e78acb354 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 21:34:46 +0000 Subject: [PATCH 54/87] Bump set-getter from 0.1.0 to 0.1.1 in /messages Bumps [set-getter](https://github.com/doowb/set-getter) from 0.1.0 to 0.1.1. - [Release notes](https://github.com/doowb/set-getter/releases) - [Commits](https://github.com/doowb/set-getter/commits/0.1.1) --- updated-dependencies: - dependency-name: set-getter dependency-type: indirect ... Signed-off-by: dependabot[bot] --- messages/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/messages/yarn.lock b/messages/yarn.lock index 564da3f4..eda1fb60 100644 --- a/messages/yarn.lock +++ b/messages/yarn.lock @@ -3473,9 +3473,9 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= set-getter@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" - integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y= + version "0.1.1" + resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.1.tgz#a3110e1b461d31a9cfc8c5c9ee2e9737ad447102" + integrity sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw== dependencies: to-object-path "^0.3.0" From 5cac4f52bc23b27772f407b324468e155bf4657b Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 22 Jun 2021 09:43:41 +0200 Subject: [PATCH 55/87] Correction form review and checks --- .../ConsoleGlobalMessageManager.svelte | 2 +- .../InputTextGlobalMessage.svelte | 24 ++++----- .../UploadAudioGlobalMessage.svelte | 52 ++++++++++++++++--- front/style/svelte-style.scss | 22 -------- 4 files changed, 57 insertions(+), 43 deletions(-) diff --git a/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte b/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte index 93385419..83837f28 100644 --- a/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte +++ b/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte @@ -2,7 +2,7 @@ import InputTextGlobalMessage from "./InputTextGlobalMessage.svelte"; import UploadAudioGlobalMessage from "./UploadAudioGlobalMessage.svelte"; import {gameManager} from "../../Phaser/Game/GameManager"; - import {Game} from "../../Phaser/Game/Game"; + import type {Game} from "../../Phaser/Game/Game"; export let game: Game; let inputSendTextActive = true; diff --git a/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte b/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte index 089c3254..1629bc7f 100644 --- a/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte +++ b/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte @@ -1,7 +1,7 @@ diff --git a/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte b/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte index 5bf581a1..e2f2443c 100644 --- a/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte +++ b/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte @@ -37,7 +37,7 @@ const fd = new FormData(); fd.append('file', selectedFile); - const res = await gameScene.connection.uploadAudio(fd); + const res = await gameScene.connection?.uploadAudio(fd); const GlobalMessage: PlayGlobalMessageInterface = { id: (res as { id: string }).id, @@ -45,7 +45,7 @@ type: AUDIO_TYPE } inputAudio.value = ''; - gameScene.connection.emitGlobalMessage(GlobalMessage); + gameScene.connection?.emitGlobalMessage(GlobalMessage); disableConsole(); } diff --git a/front/yarn.lock b/front/yarn.lock index e64a76c1..a96be8aa 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1417,6 +1417,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1428,7 +1435,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== From eb526f5b6763ce1ec81a1412bedec4f27b610ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Jun 2021 14:21:15 +0200 Subject: [PATCH 62/87] Upgrading animatedtiles plugin to trigger an event when an animation occurs and setting the scene to dirty state --- front/package.json | 2 +- front/src/Phaser/Game/GameScene.ts | 2 ++ front/yarn.lock | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/front/package.json b/front/package.json index 0a2bd161..74edf9d7 100644 --- a/front/package.json +++ b/front/package.json @@ -44,7 +44,7 @@ "generic-type-guard": "^3.2.0", "google-protobuf": "^3.13.0", "phaser": "^3.54.0", - "phaser-animated-tiles": "Informatic/phaser-animated-tiles#2d5c66a9bc426dd4cb2d856c1d599494a74f8067", + "phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254", "phaser3-rex-plugins": "^1.1.42", "queue-typescript": "^1.0.1", "quill": "1.3.6", diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5e6cfb7a..6826de8c 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -479,6 +479,8 @@ export class GameScene extends DirtyScene implements CenterListener { this.initCamera(); this.animatedTiles.init(this.Map); + this.events.on('tileanimationupdate', () => this.dirty = true); + this.initCirclesCanvas(); // Let's pause the scene if the connection is not established yet diff --git a/front/yarn.lock b/front/yarn.lock index 3f265035..f0159242 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -4098,9 +4098,9 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" -phaser-animated-tiles@Informatic/phaser-animated-tiles#2d5c66a9bc426dd4cb2d856c1d599494a74f8067: +phaser-animated-tiles@workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254: version "2.0.2" - resolved "https://codeload.github.com/Informatic/phaser-animated-tiles/tar.gz/2d5c66a9bc426dd4cb2d856c1d599494a74f8067" + resolved "https://codeload.github.com/workadventure/phaser-animated-tiles/tar.gz/da68bbededd605925621dd4f03bd27e69284b254" phaser3-rex-plugins@^1.1.42: version "1.1.47" From d87a14db0d74f48b8f3fe229e22c6997c7a967cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Jun 2021 15:18:41 +0200 Subject: [PATCH 63/87] Migrating animations documentation Migrating the animations documentation page from the website to this repository. --- docs/maps/animations.md | 33 +++++++++++++++++++++++++++++++++ docs/maps/api-ui.md | 2 +- front/yarn.lock | 9 ++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 docs/maps/animations.md diff --git a/docs/maps/animations.md b/docs/maps/animations.md new file mode 100644 index 00000000..276f2332 --- /dev/null +++ b/docs/maps/animations.md @@ -0,0 +1,33 @@ +{.section-title.accent.text-primary} +# Animating WorkAdventure maps + +A tile can run an animation in loops, for example to render water or blinking lights. Each animation frame is a single +32x32 tile. To create an animation, edit the tileset in Tiled and click on the tile to animate (or pick a free tile to +not overwrite existing ones) and click on the animation editor: + + +
+ +
+ +You can now add all tiles that should be part of the animation via drag and drop to the "playlist" and adjust the frame duration: + +
+
+ +
The tile animation editor
+
+
+ +You can preview animations directly in Tiled, using the "Show tile animations" option: + + +
+
+ +
The Show Tile Animations option
+
+
+ +{.alert.alert-info} +**Tip:** The engine does tile-updates every 100ms, animations with a shorter frame duration will most likely not look that good or may even do not work. diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index edda8613..f15de5df 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -1,5 +1,5 @@ {.section-title.accent.text-primary} -# API Room functions Reference +# API UI functions Reference ### Opening a popup diff --git a/front/yarn.lock b/front/yarn.lock index e64a76c1..a96be8aa 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1417,6 +1417,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1428,7 +1435,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== From be23db5bcf6736145638ff227cda0811ad9a6c0c Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 22 Jun 2021 16:07:31 +0200 Subject: [PATCH 64/87] Resolve import and LoadPageEvent issue --- front/src/Api/Events/IframeEvent.ts | 4 ++ front/src/Api/Events/LoadPageEvent.ts | 13 ++++++ front/src/Api/IframeListener.ts | 66 +++++++++++++++++---------- front/src/Api/iframe/nav.ts | 3 +- front/src/iframe_api.ts | 6 ++- front/yarn.lock | 9 +++- 6 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 front/src/Api/Events/LoadPageEvent.ts diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 99222e58..c3e2f6c9 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,6 +15,8 @@ import type { LayerEvent } from './LayerEvent'; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { PlaySoundEvent } from "./PlaySoundEvent"; +import type { MenuItemClickedEvent } from "./MenuItemClickedEvent"; +import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -44,6 +46,8 @@ export type IframeEventMap = { loadSound: LoadSoundEvent playSound: PlaySoundEvent stopSound: null, + getState: undefined, + registerMenuCommand: undefined } export interface IframeEvent { type: T; diff --git a/front/src/Api/Events/LoadPageEvent.ts b/front/src/Api/Events/LoadPageEvent.ts new file mode 100644 index 00000000..9bc7f32a --- /dev/null +++ b/front/src/Api/Events/LoadPageEvent.ts @@ -0,0 +1,13 @@ +import * as tg from "generic-type-guard"; + + + +export const isLoadPageEvent = + new tg.IsInterface().withProperties({ + url: tg.isString, + }).get(); + +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type LoadPageEvent = tg.GuardedType; \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index e7638d04..a495d92b 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,21 +1,36 @@ - -import { Subject } from "rxjs"; -import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; -import { HtmlUtils } from "../WebRtc/HtmlUtils"; -import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; -import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; -import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; -import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; -import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; -import { scriptUtils } from "./ScriptUtils"; -import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; -import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; -import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; -import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; -import { isLoadPageEvent } from './Events/LoadPageEvent'; +import {Subject} from "rxjs"; +import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; +import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; +import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; +import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; +import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; +import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; +import {scriptUtils} from "./ScriptUtils"; +import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; +import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { + IframeEvent, + IframeEventMap, + IframeResponseEvent, + IframeResponseEventMap, + isIframeEventWrapper, + TypedMessageEvent +} from "./Events/IframeEvent"; +import type {UserInputChatEvent} from "./Events/UserInputChatEvent"; +//import { isLoadPageEvent } from './Events/LoadPageEvent'; import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; +import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; +import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; +import {isMenuItemRegisterEvent} from "./Events/MenuItemRegisterEvent"; +import type {DataLayerEvent} from "./Events/DataLayerEvent"; +import type {GameStateEvent} from "./Events/GameStateEvent"; +import type {MenuItemClickedEvent} from "./Events/MenuItemClickedEvent"; +import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent"; +import {isLoadPageEvent} from "./Events/LoadPageEvent"; + /** * Listens to messages from iframes and turn those messages into easy to use observables. * Also allows to send messages to those iframes. @@ -34,6 +49,9 @@ class IframeListener { private readonly _goToPageStream: Subject = new Subject(); public readonly goToPageStream = this._goToPageStream.asObservable(); + private readonly _loadPageStream: Subject = new Subject(); + public readonly loadPageStream = this._loadPageStream.asObservable(); + private readonly _openCoWebSiteStream: Subject = new Subject(); public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable(); @@ -135,6 +153,9 @@ class IframeListener { else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { scriptUtils.goToPage(payload.data.url); } + else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) { + this._loadPageStream.next(payload.data.url); + } else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) { this._playSoundStream.next(payload.data); } @@ -216,7 +237,7 @@ class IframeListener { if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { // Using external iframe mode ( const iframe = document.createElement('iframe'); - iframe.id = this.getIFrameId(scriptUrl); + iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.style.display = 'none'; iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl); @@ -231,25 +252,24 @@ class IframeListener { } else { // production code const iframe = document.createElement('iframe'); - iframe.id = this.getIFrameId(scriptUrl); + iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.style.display = 'none'; // We are putting a sandbox on this script because it will run in the same domain as the main website. iframe.sandbox.add('allow-scripts'); iframe.sandbox.add('allow-top-navigation-by-user-activation'); - const html = '\n' + + //iframe.src = "data:text/html;charset=utf-8," + escape(html); + iframe.srcdoc = '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + + '\n' + '\n' + '\n'; - //iframe.src = "data:text/html;charset=utf-8," + escape(html); - iframe.srcdoc = html; - document.body.prepend(iframe); this.scripts.set(scriptUrl, iframe); @@ -259,12 +279,12 @@ class IframeListener { } - private getIFrameId(scriptUrl: string): string { + private static getIFrameId(scriptUrl: string): string { return 'script' + btoa(scriptUrl); } unregisterScript(scriptUrl: string): void { - const iFrameId = this.getIFrameId(scriptUrl); + const iFrameId = IframeListener.getIFrameId(scriptUrl); const iframe = HtmlUtils.getElementByIdOrFail(iFrameId); if (!iframe) { throw new Error('Unknown iframe for script "' + scriptUrl + '"'); diff --git a/front/src/Api/iframe/nav.ts b/front/src/Api/iframe/nav.ts index b6798330..f6add88e 100644 --- a/front/src/Api/iframe/nav.ts +++ b/front/src/Api/iframe/nav.ts @@ -2,6 +2,7 @@ import type { GoToPageEvent } from '../Events/GoToPageEvent'; import type { OpenTabEvent } from '../Events/OpenTabEvent'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent"; +import type {LoadPageEvent} from "../Events/LoadPageEvent"; class WorkadventureNavigationCommands extends IframeApiContribution { @@ -31,7 +32,7 @@ class WorkadventureNavigationCommands extends IframeApiContribution void> = new Map() const wa = { ui, nav, @@ -100,7 +103,8 @@ const wa = { /** * @deprecated Use WA.nav.openCoWebSite instead - */openCoWebSite(url: string): void { + */ + openCoWebSite(url: string): void { console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead'); nav.openCoWebSite(url); }, diff --git a/front/yarn.lock b/front/yarn.lock index e64a76c1..a96be8aa 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1417,6 +1417,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1428,7 +1435,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== From 67d4c6e271486d83f2503aebdd67658e46ed15ba Mon Sep 17 00:00:00 2001 From: GRL Date: Tue, 22 Jun 2021 16:12:53 +0200 Subject: [PATCH 65/87] Resolve review --- front/src/Components/App.svelte | 4 ++-- .../InputTextGlobalMessage.svelte | 10 +++++----- .../UploadAudioGlobalMessage.svelte | 6 +++--- front/src/Phaser/Menu/MenuScene.ts | 10 +++++----- front/src/Stores/ConsoleGlobalMessageManagerStore.ts | 4 ++-- front/src/Stores/UserInputStore.ts | 8 ++++---- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index 72173398..2e159d2d 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -21,7 +21,7 @@ import AudioPlaying from "./UI/AudioPlaying.svelte"; import {soundPlayingStore} from "../Stores/SoundPlayingStore"; import ErrorDialog from "./UI/ErrorDialog.svelte"; - import {ConsoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore"; + import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore"; import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte"; export let game: Game; @@ -72,7 +72,7 @@ {/if} - {#if $ConsoleGlobalMessageManagerVisibleStore} + {#if $consoleGlobalMessageManagerVisibleStore}
diff --git a/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte b/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte index 889c8f6d..c11b4b0e 100644 --- a/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte +++ b/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte @@ -1,5 +1,5 @@ diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index 2d848467..98687c65 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -11,7 +11,7 @@ import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Co import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {menuIconVisible} from "../../Stores/MenuStore"; import {videoConstraintStore} from "../../Stores/MediaStore"; -import {ConsoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; +import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; import {get} from "svelte/store"; export const MenuSceneName = 'MenuScene'; @@ -161,7 +161,7 @@ export class MenuScene extends Phaser.Scene { this.sideMenuOpened = false; this.closeAll(); this.menuButton.getChildByID('openMenuButton').innerHTML = ``; - ConsoleGlobalMessageManagerVisibleStore.set(false); + consoleGlobalMessageManagerVisibleStore.set(false); this.tweens.add({ targets: this.menuElement, x: closedSideMenuX, @@ -306,10 +306,10 @@ export class MenuScene extends Phaser.Scene { this.toggleFullscreen(); break; case 'adminConsoleButton': - if (get(ConsoleGlobalMessageManagerVisibleStore)) { - ConsoleGlobalMessageManagerVisibleStore.set(false); + if (get(consoleGlobalMessageManagerVisibleStore)) { + consoleGlobalMessageManagerVisibleStore.set(false); } else { - ConsoleGlobalMessageManagerVisibleStore.set(true); + consoleGlobalMessageManagerVisibleStore.set(true); } break; } diff --git a/front/src/Stores/ConsoleGlobalMessageManagerStore.ts b/front/src/Stores/ConsoleGlobalMessageManagerStore.ts index 4c557d71..1fa04bfe 100644 --- a/front/src/Stores/ConsoleGlobalMessageManagerStore.ts +++ b/front/src/Stores/ConsoleGlobalMessageManagerStore.ts @@ -1,5 +1,5 @@ import { writable } from "svelte/store"; -export const ConsoleGlobalMessageManagerVisibleStore = writable(false); +export const consoleGlobalMessageManagerVisibleStore = writable(false); -export const ConsoleGlobalMessageManagerFocusStore = writable(false); \ No newline at end of file +export const consoleGlobalMessageManagerFocusStore = writable(false); \ No newline at end of file diff --git a/front/src/Stores/UserInputStore.ts b/front/src/Stores/UserInputStore.ts index b5b4de30..cbb7f0c3 100644 --- a/front/src/Stores/UserInputStore.ts +++ b/front/src/Stores/UserInputStore.ts @@ -1,10 +1,10 @@ import {derived} from "svelte/store"; -import {ConsoleGlobalMessageManagerFocusStore,} from "./ConsoleGlobalMessageManagerStore"; +import {consoleGlobalMessageManagerFocusStore} from "./ConsoleGlobalMessageManagerStore"; //derived from the focus on Menu, ConsoleGlobal, Chat and ... export const enableUserInputsStore = derived( - ConsoleGlobalMessageManagerFocusStore, - ($ConsoleGlobalMessageManagerFocusStore) => { - return !$ConsoleGlobalMessageManagerFocusStore; + consoleGlobalMessageManagerFocusStore, + ($consoleGlobalMessageManagerFocusStore) => { + return !$consoleGlobalMessageManagerFocusStore; } ); \ No newline at end of file From e9dd7ebdd9343b71239d0f91701767cc3480db04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Jun 2021 16:35:57 +0200 Subject: [PATCH 66/87] Enabling Outline back on actionable objects Actionable objects (still a prototype) were outlined when you walk next to them. The OutlinePipeline was broken when moving in Phaser 3.50+. This PR completely removes the custom OutlinePipeline and replaces it with the rexOutlinePipelinePlugin that is provided by a third party library and that works great \o/ --- front/src/Phaser/Items/ActionableItem.ts | 19 ++++--- front/src/Phaser/Shaders/OutlinePipeline.ts | 59 --------------------- front/src/index.ts | 11 ++-- front/src/rex-plugins.d.ts | 12 ++++- 4 files changed, 29 insertions(+), 72 deletions(-) delete mode 100644 front/src/Phaser/Shaders/OutlinePipeline.ts diff --git a/front/src/Phaser/Items/ActionableItem.ts b/front/src/Phaser/Items/ActionableItem.ts index f012b525..7c7090b0 100644 --- a/front/src/Phaser/Items/ActionableItem.ts +++ b/front/src/Phaser/Items/ActionableItem.ts @@ -3,8 +3,8 @@ * It has coordinates and an "activation radius" */ import Sprite = Phaser.GameObjects.Sprite; -import {OutlinePipeline} from "../Shaders/OutlinePipeline"; import type {GameScene} from "../Game/GameScene"; +import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; type EventCallback = (state: unknown, parameters: unknown) => void; @@ -42,11 +42,11 @@ export class ActionableItem { return; } this.isSelectable = true; - if (this.sprite.pipeline) { - // Commented out to try to fix MacOS issue - /*this.sprite.setPipeline(OutlinePipeline.KEY); - this.sprite.pipeline.set2f('uTextureSize', this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height);*/ - } + + this.getOutlinePlugin()?.add(this.sprite, { + thickness: 2, + outlineColor: 0xffff00 + }); } /** @@ -57,8 +57,11 @@ export class ActionableItem { return; } this.isSelectable = false; - // Commented out to try to fix MacOS issue - //this.sprite.resetPipeline(); + this.getOutlinePlugin()?.remove(this.sprite); + } + + private getOutlinePlugin(): OutlinePipelinePlugin|undefined { + return this.sprite.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined; } /** diff --git a/front/src/Phaser/Shaders/OutlinePipeline.ts b/front/src/Phaser/Shaders/OutlinePipeline.ts deleted file mode 100644 index 0d074bc3..00000000 --- a/front/src/Phaser/Shaders/OutlinePipeline.ts +++ /dev/null @@ -1,59 +0,0 @@ -export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.MultiPipeline { - - // the unique id of this pipeline - public static readonly KEY = 'Outline'; - - /** - * @param {Phaser.Game} game - the controller of the game instance - */ - constructor(game: Phaser.Game) - { - super({ - game: game, - fragShader: ` - precision mediump float; - - uniform sampler2D uMainSampler; - uniform vec2 uTextureSize; - - varying vec2 outTexCoord; - varying float outTintEffect; - varying vec4 outTint; - - void main(void) - { - vec4 texture = texture2D(uMainSampler, outTexCoord); - vec4 texel = vec4(outTint.rgb * outTint.a, outTint.a); - vec4 color = texture; - - if (outTintEffect == 0.0) - { - color = texture * texel; - } - else if (outTintEffect == 1.0) - { - color.rgb = mix(texture.rgb, outTint.rgb * outTint.a, texture.a); - color.a = texture.a * texel.a; - } - else if (outTintEffect == 2.0) - { - color = texel; - } - - vec2 onePixel = vec2(1.0, 1.0) / uTextureSize; - float upAlpha = texture2D(uMainSampler, outTexCoord + vec2(0.0, onePixel.y)).a; - float leftAlpha = texture2D(uMainSampler, outTexCoord + vec2(-onePixel.x, 0.0)).a; - float downAlpha = texture2D(uMainSampler, outTexCoord + vec2(0.0, -onePixel.y)).a; - float rightAlpha = texture2D(uMainSampler, outTexCoord + vec2(onePixel.x, 0.0)).a; - - if (texture.a == 0.0 && max(max(upAlpha, downAlpha), max(leftAlpha, rightAlpha)) == 1.0) - { - color = vec4(1.0, 1.0, 0.0, 1.0); - } - - gl_FragColor = color; - } - ` - }); - } -} diff --git a/front/src/index.ts b/front/src/index.ts index 90d4c612..59e748b4 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -10,6 +10,7 @@ import {SelectCompanionScene} from "./Phaser/Login/SelectCompanionScene"; import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene"; import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; import WebFontLoaderPlugin from 'phaser3-rex-plugins/plugins/webfontloader-plugin.js'; +import OutlinePipelinePlugin from 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js'; import {EntryScene} from "./Phaser/Login/EntryScene"; import {coWebsiteManager} from "./WebRtc/CoWebsiteManager"; import {MenuScene} from "./Phaser/Menu/MenuScene"; @@ -22,6 +23,8 @@ import {waScaleManager} from "./Phaser/Services/WaScaleManager"; import {Game} from "./Phaser/Game/Game"; import App from './Components/App.svelte'; import {HtmlUtils} from "./WebRtc/HtmlUtils"; +import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer; + const {width, height} = coWebsiteManager.getGameSize(); @@ -123,11 +126,11 @@ const config: GameConfig = { powerPreference: "low-power", callbacks: { postBoot: game => { - // Commented out to try to fix MacOS bug - /*const renderer = game.renderer; + // Install rexOutlinePipeline only if the renderer is WebGL. + const renderer = game.renderer; if (renderer instanceof WebGLRenderer) { - renderer.pipelines.add(OutlinePipeline.KEY, new OutlinePipeline(game)); - }*/ + game.plugins.install('rexOutlinePipeline', OutlinePipelinePlugin, true); + } } } }; diff --git a/front/src/rex-plugins.d.ts b/front/src/rex-plugins.d.ts index 2e160315..8c8a9fc1 100644 --- a/front/src/rex-plugins.d.ts +++ b/front/src/rex-plugins.d.ts @@ -1,4 +1,3 @@ - declare module 'phaser3-rex-plugins/plugins/virtualjoystick.js' { const content: any; // eslint-disable-line export default content; @@ -11,6 +10,17 @@ declare module 'phaser3-rex-plugins/plugins/webfontloader-plugin.js' { const content: any; // eslint-disable-line export default content; } +declare module 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js' { + import GameObject = Phaser.GameObjects.GameObject; + + class OutlinePipelinePlugin { + add(gameObject: GameObject, config: object); + + remove(gameObject: GameObject, name?: string); + } + + export default OutlinePipelinePlugin; +} declare module 'phaser3-rex-plugins/plugins/gestures.js' { export const Pinch: any; // eslint-disable-line } From dc0f3feabfc12fb1e7c046c19070917be71bb289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Jun 2021 17:15:18 +0200 Subject: [PATCH 67/87] Putting an outline on the character name In the future, we might want to put an outline on the whole character body but this is harder as the body is actually a container and so we would need to turn this container into a sprite first. --- front/src/Phaser/Entity/Character.ts | 21 +++++++++++++++++++++ front/src/Phaser/Game/DirtyScene.ts | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 2ff66178..8a82afc3 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -8,6 +8,8 @@ import {Companion} from "../Companion/Companion"; import type {GameScene} from "../Game/GameScene"; import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; import {waScaleManager} from "../Services/WaScaleManager"; +import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; +import * as Phaser from "phaser"; const playerNameY = - 25; @@ -32,6 +34,7 @@ export abstract class Character extends Container { public companion?: Companion; private emote: Phaser.GameObjects.Sprite | null = null; private emoteTween: Phaser.Tweens.Tween|null = null; + scene: GameScene; constructor(scene: GameScene, x: number, @@ -46,6 +49,7 @@ export abstract class Character extends Container { companionTexturePromise?: Promise ) { super(scene, x, y/*, texture, frame*/); + this.scene = scene; this.PlayerValue = name; this.invisible = true @@ -67,6 +71,19 @@ export abstract class Character extends Container { hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method useHandCursor: true, }); + + this.on('pointerover',() => { + this.getOutlinePlugin()?.add(this.playerName, { + thickness: 2, + outlineColor: 0xffff00 + }); + this.scene.markDirty(); + }); + this.on('pointerout',() => { + this.getOutlinePlugin()?.remove(this.playerName); + this.scene.markDirty(); + }) + } scene.add.existing(this); @@ -86,6 +103,10 @@ export abstract class Character extends Container { } } + private getOutlinePlugin(): OutlinePipelinePlugin|undefined { + return this.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined; + } + public addCompanion(name: string, texturePromise?: Promise): void { if (typeof texturePromise !== 'undefined') { this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); diff --git a/front/src/Phaser/Game/DirtyScene.ts b/front/src/Phaser/Game/DirtyScene.ts index 3e1f3cdf..70cbb127 100644 --- a/front/src/Phaser/Game/DirtyScene.ts +++ b/front/src/Phaser/Game/DirtyScene.ts @@ -4,6 +4,7 @@ import Events = Phaser.Scenes.Events; import AnimationEvents = Phaser.Animations.Events; import StructEvents = Phaser.Structs.Events; import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable"; +import Phaser from "phaser"; /** * A scene that can track its dirty/pristine state. @@ -69,6 +70,10 @@ export abstract class DirtyScene extends ResizableScene { return this.dirty || this.objectListChanged; } + public markDirty(): void { + this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => this.dirty = true); + } + public onResize(): void { this.objectListChanged = true; } From 1ef1a1cb22034aa756000a7c9688f806f69b7426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Jun 2021 17:47:54 +0200 Subject: [PATCH 68/87] Removing useless import --- front/src/Phaser/Entity/Character.ts | 1 - front/src/Phaser/Game/DirtyScene.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 8a82afc3..7263a584 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -9,7 +9,6 @@ import type {GameScene} from "../Game/GameScene"; import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; import {waScaleManager} from "../Services/WaScaleManager"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; -import * as Phaser from "phaser"; const playerNameY = - 25; diff --git a/front/src/Phaser/Game/DirtyScene.ts b/front/src/Phaser/Game/DirtyScene.ts index 70cbb127..04ec2474 100644 --- a/front/src/Phaser/Game/DirtyScene.ts +++ b/front/src/Phaser/Game/DirtyScene.ts @@ -4,7 +4,6 @@ import Events = Phaser.Scenes.Events; import AnimationEvents = Phaser.Animations.Events; import StructEvents = Phaser.Structs.Events; import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable"; -import Phaser from "phaser"; /** * A scene that can track its dirty/pristine state. From 321fff24e61195c7156feaa36b4f8d9f6a8817dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Jun 2021 18:09:55 +0200 Subject: [PATCH 69/87] Filled the Changelog with past changes --- CHANGELOG.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd2c973..13335737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,19 @@ -## Version 1.3.9 - in dev +## Version 1.4.x-dev + +### Updates + +- Added the ability to have animated tiles in maps #1216 #1217 +- Enabled outlines on actionable item again (they were disabled when migrating to Phaser 3.50) #1218 +- Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219 +- Migrated the admin console to Svelte, and redesigned the console #1211 + +## Version 1.4.1 + +### Bugfixes + +- Loading errors after the preload stage should not crash the game anymore + +## Version 1.4.0 ### BREAKING CHANGES From bdc4b43c17e8ca86598f9e4668ef4ea1ad36fd3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 10:10:53 +0200 Subject: [PATCH 70/87] Setting up prettier In order not to wreak havoc on all the open PRs, Prettier will be added slowly, on every changed file, thanks to Husky (for a precommit hook) and lint-staged, for linting only staged files. --- .husky/.gitignore | 1 + .husky/pre-commit | 4 + front/package.json | 9 ++ front/yarn.lock | 229 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 8 ++ yarn.lock | 7 ++ 6 files changed, 249 insertions(+), 9 deletions(-) create mode 100644 .husky/.gitignore create mode 100755 .husky/pre-commit create mode 100644 package.json create mode 100644 yarn.lock diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 00000000..31354ec1 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..e43d66b5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +cd front && yarn run precommit diff --git a/front/package.json b/front/package.json index 74edf9d7..e2702c28 100644 --- a/front/package.json +++ b/front/package.json @@ -18,9 +18,11 @@ "fork-ts-checker-webpack-plugin": "^6.2.9", "html-webpack-plugin": "^5.3.1", "jasmine": "^3.5.0", + "lint-staged": "^11.0.0", "mini-css-extract-plugin": "^1.6.0", "node-polyfill-webpack-plugin": "^1.1.2", "npm-run-all": "^4.1.5", + "prettier": "^2.3.1", "sass": "^1.32.12", "sass-loader": "^11.1.0", "svelte": "^3.38.2", @@ -60,7 +62,14 @@ "test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", + "precommit": "lint-staged", "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch", "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"" + }, + "lint-staged": { + "*.ts": [ + "prettier --write", + "git add" + ] } } diff --git a/front/yarn.lock b/front/yarn.lock index f0159242..fec87661 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -569,6 +569,14 @@ after@0.8.2: resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -609,6 +617,13 @@ ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -1121,7 +1136,7 @@ chalk@^2.0.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== @@ -1208,6 +1223,26 @@ clean-css@^4.2.3: dependencies: source-map "~0.6.0" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -1278,7 +1313,7 @@ commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^7.0.0: +commander@^7.0.0, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== @@ -1381,6 +1416,17 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -1536,6 +1582,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + deep-equal@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -1823,7 +1874,7 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5: +enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -2426,6 +2477,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -2822,6 +2878,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -3060,6 +3121,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-path-cwd@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" @@ -3099,6 +3165,11 @@ is-regex@^1.0.4, is-regex@^1.1.2: call-bind "^1.0.2" has-symbols "^1.0.2" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3132,6 +3203,11 @@ is-typed-array@^1.1.3: foreach "^2.0.5" has-symbols "^1.0.1" +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -3309,6 +3385,40 @@ linked-list-typescript@^1.0.11: resolved "https://registry.yarnpkg.com/linked-list-typescript/-/linked-list-typescript-1.0.15.tgz#faeed93cf9203f102e2158c29edcddda320abe82" integrity sha512-RIyUu9lnJIyIaMe63O7/aFv/T2v3KsMFuXMBbUQCHX+cgtGro86ETDj5ed0a8gQL2+DFjzYYsgVG4I36/cUwgw== +lint-staged@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" + integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + dedent "^0.7.0" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -3363,6 +3473,24 @@ lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + loglevel@^1.6.8: version "1.7.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" @@ -3477,7 +3605,7 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0, micromatch@^4.0.2: +micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -3842,7 +3970,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -3918,6 +4046,13 @@ p-map@^2.0.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-retry@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" @@ -4171,6 +4306,13 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + portfinder@^1.0.26: version "1.0.28" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" @@ -4240,6 +4382,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + pretty-error@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" @@ -4569,6 +4716,14 @@ resolve@^1.10.0, resolve@^1.9.0: is-core-module "^2.2.0" path-parse "^1.0.6" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -4613,7 +4768,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^6.6.3: +rxjs@^6.6.3, rxjs@^6.6.7: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -4703,6 +4858,11 @@ selfsigned@^1.10.8: dependencies: node-forge "^0.10.0" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + "semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -4843,7 +5003,7 @@ shell-quote@^1.6.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -signal-exit@^3.0.0, signal-exit@^3.0.3: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -4866,6 +5026,15 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -5097,6 +5266,11 @@ stream-http@^3.2.0: readable-stream "^3.6.0" xtend "^4.0.2" +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -5106,7 +5280,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== @@ -5154,6 +5328,15 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -5329,6 +5512,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -5449,6 +5637,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -5836,6 +6029,24 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -5873,7 +6084,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: +yaml@^1.10.0, yaml@^1.7.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== diff --git a/package.json b/package.json new file mode 100644 index 00000000..ea1d9854 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "husky": "^6.0.0" + }, + "scripts": { + "prepare": "husky install" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..166b84c7 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,7 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +husky@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" From af67d9b5131667f5718339424e1d4b1ef29d3b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 10:44:55 +0200 Subject: [PATCH 71/87] Migrating WA-Maps page to this repo and documenting the fact that tiles properties can now be used. --- CHANGELOG.md | 1 + docs/maps/wa-maps.md | 90 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 docs/maps/wa-maps.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 13335737..560365f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enabled outlines on actionable item again (they were disabled when migrating to Phaser 3.50) #1218 - Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219 - Migrated the admin console to Svelte, and redesigned the console #1211 +- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1) ## Version 1.4.1 diff --git a/docs/maps/wa-maps.md b/docs/maps/wa-maps.md new file mode 100644 index 00000000..98fdce2d --- /dev/null +++ b/docs/maps/wa-maps.md @@ -0,0 +1,90 @@ +{.section-title.accent.text-primary} +# About WorkAdventure maps + +A WorkAdventure map is a map in "JSON" format generated by [Tiled](https://www.mapeditor.org/). + +## Tiles + +A map is made of "tiles" (we can also call them "sprites"). In WorkAdventure, the tiles are small images of 32x32 pixels. + +Tiles may have transparent parts. Many tiles can be stored in a single PNG file. We call this file a "tileset". + +There are many tilesets available on the internet. Some examples of websites offering awesome tiles: + +* [itch.io](https://itch.io/) +* [opengameart.org](https://opengameart.org/) +* [deviantart.com](https://www.deviantart.com/) + +Keep in mind the size of tiles and do not forget to check the license of the tileset you are using! + + +## How to design "pixel" tiles + +You can design your own tiles as well as change existing tiles, this is usually referred to as "pixeling". You can start drawing your own tiles with [Piskel](https://www.piskelapp.com/). It is easy to use and well targeted at "pixeling". If you are getting serious about pixeling, the awesome folks at the Chaos Computer Club recommend the use of the editor [Krita](https://krita.org/). There are plenty of other editors as well. + +If you are using Krita: + +* Please double check that your tiles are 32x32 pixels in size. You can enable a grid under view -> show grid and under settings -> dockers -> grid you can select the grid size. +* Use transparency if you have to model transitions between different materials. This is more flexible and saves you time by not modeling every transition. +* You can follow the Pixel-Art Workshop by blinry: [media.ccc.de/v/34C3-jugend-hackt-1016-pixel_art_workshop](https://media.ccc.de/v/34C3-jugend-hackt-1016-pixel_art_workshop) + +## WorkAdventure Map Rules + +In order to design a map that will be readable by WorkAdventure, you will have to respect some constraints. + +In particular, you will need to: + +* set a start position for the players +* configure the "floor layer" (so that WorkAdventure can correctly display characters above the floor, but under the ceiling) +* eventually, you can place exits that link to other maps + +A few things to notice: + +* your map can have as many layers as you want +* your map MUST contain a layer named "floorLayer" of type "objectgroup" that represents the layer on which characters will be drawn. Every layer above the "floorLayer" will be displayed on top of the characters. +* the tilesets in your map MUST be embedded. You cannot refer to an external typeset in a TSX file. Click the "embed tileset" button in the tileset tab to embed tileset data. +* your map MUST be exported in JSON format. You need to use a recent version of Tiled to get JSON format export (1.3+) +* WorkAdventure doesn't support object layers and will ignore them +* If you are starting from a blank map, your map MUST be orthogonal and tiles size should be 32x32. + +
+
+ +
"floorLayer" is compulsory
+
+
+ +## Building walls and "collidable" areas + +By default, the characters can traverse any tiles. If you want to prevent your characeter from going through a tile (like a wall or a desktop), you must make this tile "collidable". You can do this by settings the `collides` property on a given tile. + +To make a tile "collidable", you should: + +1. select the relevant tileset and switch to "edit" mode: + {.document-img} + ![](https://workadventu.re/img/docs/collides-1.png) +2. right click on a tile of the tileset to select it: + {.document-img} + ![](https://workadventu.re/img/docs/collides-2.png) +3. on the left pane in the custom properties section, right click and select "Add properties": + {.document-img} + ![](https://workadventu.re/img/docs/collides-3.png) + + Please add a `collides` property. The type of the property must be **bool**. + +4. finally, check the checkbox for the `collides` property: + {.document-img} + ![](https://workadventu.re/img/docs/collides-4.png) + +Repeat for every tile that should be "collidable". + +## Adding behaviour with properties + +In the next sections, you will see how you can add behaviour on your map by adding "properties". +You can add properties for a variety of features: putting exits, opening websites, meeting rooms, silent zones, etc... + +You can add properties either on individual tiles of a tileset OR on a complete layer. + +If you put a property on a layer, it will be triggered if your Woka walks on any tile of the layer. + +The exception is the "collides" property that can only be set on tiles, but not on a complete layer. From bdb32a29e184f0c3e2f40266fcbb23e6efa88451 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 23 Jun 2021 11:32:11 +0200 Subject: [PATCH 72/87] New methods refactored --- CHANGELOG.md | 16 ++-- docs/maps/api-player.md | 21 +++++ docs/maps/api-reference.md | 118 +----------------------- docs/maps/api-room.md | 81 ++++++++++++++++ docs/maps/api-ui.md | 9 +- docs/maps/scripting.md | 2 +- front/src/Api/IframeListener.ts | 2 +- front/src/Api/iframe/player.ts | 29 ++++++ front/src/Api/iframe/room.ts | 91 +++++++++++++++++- front/src/Phaser/Game/GameScene.ts | 2 +- front/src/iframe_api.ts | 2 + maps/tests/Metadata/getCurrentRoom.html | 2 +- maps/tests/Metadata/getCurrentUser.html | 2 +- maps/tests/Metadata/playerMove.html | 2 +- maps/tests/Metadata/setProperty.html | 4 +- maps/tests/Metadata/showHideLayer.html | 4 +- 16 files changed, 246 insertions(+), 141 deletions(-) create mode 100644 docs/maps/api-player.md create mode 100644 front/src/Api/iframe/player.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9d743e..fb4f6d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ - Enabled outlines on actionable item again (they were disabled when migrating to Phaser 3.50) #1218 - Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219 - Migrated the admin console to Svelte, and redesigned the console #1211 +- New scripting API features : + - Use `WA.room.showLayer(): void` to show a layer + - Use `WA.room.hideLayer(): void` to hide a layer + - Use `WA.room.setProperty() : void` to add or change existing property of a layer + - Use `WA.player.onPlayerMove(): void` to track the movement of the current player + - Use `WA.room.getCurrentUser(): Promise` to get the ID, name and tags of the current player + - Use `WA.room.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started + - Use `WA.ui.registerMenuCommand(): void` to add a custom menu ## Version 1.4.1 @@ -48,13 +56,7 @@ - Added a new `DISPLAY_TERMS_OF_USE` environment variable to trigger the display of terms of use - New scripting API features: - Use `WA.loadSound(): Sound` to load / play / stop a sound - - Use `WA.showLayer(): void` to show a layer - - Use `WA.hideLayer(): void` to hide a layer - - Use `WA.setProperty() : void` to add or change existing property of a layer - - Use `WA.onPlayerMove(): void` to track the movement of the current player - - Use `WA.getCurrentUser(): Promise` to get the ID, name and tags of the current player - - Use `WA.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - - Use `WA.registerMenuCommand(): void` to add a custom menu + ### Bug Fixes diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md new file mode 100644 index 00000000..f483731e --- /dev/null +++ b/docs/maps/api-player.md @@ -0,0 +1,21 @@ +{.section-title.accent.text-primary} +# API Player functions Reference + +### Listen to player movement +``` +WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; +``` +Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction. + +The event has the following attributes : +* **moving (boolean):** **true** when the current player is moving, **false** otherwise. +* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. +* **x (number):** coordinate X of the current player. +* **y (number):** coordinate Y of the current player. + +**callback:** the function that will be called when the current player is moving. It contains the event. + +Example : +```javascript +WA.player.onPlayerMove(console.log); +``` \ No newline at end of file diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index a39ec1db..30a11b2a 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -4,123 +4,9 @@ - [Navigation functions](api-nav.md) - [Chat functions](api-chat.md) - [Room functions](api-room.md) +- [Player functions](api-player.md) - [UI functions](api-ui.md) - [Sound functions](api-sound.md) - [Controls functions](api-controls.md) -- [List of deprecated functions](api-deprecated.md) - -### Show / Hide a layer -``` -WA.showLayer(layerName : string): void -WA.hideLayer(layerName : string) : void -``` -These 2 methods can be used to show and hide a layer. - -Example : -```javascript -WA.showLayer('bottom'); -//... -WA.hideLayer('bottom'); -``` - -### Set/Create properties in a layer - -``` -WA.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; -``` - -Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. - -Example : -```javascript -WA.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); -``` - -### Listen to player movement - -``` -onPlayerMove(callback: HasPlayerMovedEventCallback): void; -``` -Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction. - -The event has the following attributes : -* **moving (boolean):** **true** when the current player is moving, **false** otherwise. -* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. -* **x (number):** coordinate X of the current player. -* **y (number):** coordinate Y of the current player. - -**callback:** the function that will be called when the current player is moving. It contains the event. - -Example : -```javascript -WA.onPlayerMove(console.log); -``` - -### Getting informations on the current user -``` -getCurrentUser(): Promise -``` -Return a promise that resolves to a `User` object with the following attributes : -* **id (string) :** ID of the current user -* **nickName (string) :** name displayed above the current user -* **tags (string[]) :** list of all the tags of the current user - -Example : -```javascript -WA.getCurrentUser().then((user) => { - if (user.nickName === 'ABC') { - console.log(user.tags); - } -}) -``` - -### Getting informations on the current room -``` -getCurrentRoom(): Promise -``` -Return a promise that resolves to a `Room` object with the following attributes : -* **id (string) :** ID of the current room -* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. -* **mapUrl (string) :** Url of the JSON map file -* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer - -Example : -```javascript -WA.getCurrentRoom((room) => { - if (room.id === '42') { - console.log(room.map); - window.open(room.mapUrl, '_blank'); - } -}) -``` - -### Add a custom menu -``` -registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void): void -``` -Add a custom menu item containing the text `commandDescriptor`. A click on the menu will trigger the `callback`. - -Example : -```javascript -WA.registerMenuCommand('About', () => { - console.log("The About menu was clicked"); -}); -``` - - -### Working with group layers -If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together. - -Example : -
-
- -
-
- -The name of the layers of this map are : -* `entries/start` -* `bottom/ground/under` -* `bottom/build/carpet` -* `wall` \ No newline at end of file +- [List of deprecated functions](api-deprecated.md) \ No newline at end of file diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index dc7a8612..d8381cc6 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -1,6 +1,22 @@ {.section-title.accent.text-primary} # API Room functions Reference +### Working with group layers +If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together. + +Example : +
+
+ +
+
+ +The name of the layers of this map are : +* `entries/start` +* `bottom/ground/under` +* `bottom/build/carpet` +* `wall` + ### Detecting when the user enters/leaves a zone ``` @@ -31,3 +47,68 @@ WA.room.onLeaveZone('myZone', () => { WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); }) ``` + +### Show / Hide a layer +``` +WA.room.showLayer(layerName : string): void +WA.room.hideLayer(layerName : string) : void +``` +These 2 methods can be used to show and hide a layer. + +Example : +```javascript +WA.room.showLayer('bottom'); +//... +WA.room.hideLayer('bottom'); +``` + +### Set/Create properties in a layer + +``` +WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; +``` + +Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. + +Example : +```javascript +WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); +``` + +### Getting information on the current room +``` +WA.room.getCurrentRoom(): Promise +``` +Return a promise that resolves to a `Room` object with the following attributes : +* **id (string) :** ID of the current room +* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called. +* **mapUrl (string) :** Url of the JSON map file +* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer + +Example : +```javascript +WA.room.getCurrentRoom((room) => { + if (room.id === '42') { + console.log(room.map); + window.open(room.mapUrl, '_blank'); + } +}) +``` + +### Getting information on the current user +``` +WA.player.getCurrentUser(): Promise +``` +Return a promise that resolves to a `User` object with the following attributes : +* **id (string) :** ID of the current user +* **nickName (string) :** name displayed above the current user +* **tags (string[]) :** list of all the tags of the current user + +Example : +```javascript +WA.room.getCurrentUser().then((user) => { + if (user.nickName === 'ABC') { + console.log(user.tags); + } +}) +``` diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index 44248b53..fc38dc41 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -66,16 +66,15 @@ WA.room.onLeaveZone('myZone', () => { }); ``` -### register additional menu entries - -adds an additional Entry to the main menu , these exist until the map is unloaded - +### Add custom menu ```typescript WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void ``` -Example: +Add a custom menu item containing the text `commandDescriptor` in the main menu. A click on the menu will trigger the `callback`. +Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script. +Example: ```javascript diff --git a/docs/maps/scripting.md b/docs/maps/scripting.md index b9dee484..5f645b81 100644 --- a/docs/maps/scripting.md +++ b/docs/maps/scripting.md @@ -101,7 +101,7 @@ You can now start by testing this with a simple message sent to the chat. ```html ... ... ``` diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index c4aa29b1..9311d7b6 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -204,7 +204,7 @@ class IframeListener { } - sendFrozenGameStateEvent(gameStateEvent: GameStateEvent) { + sendGameStateEvent(gameStateEvent: GameStateEvent) { this.postMessage({ 'type': 'gameState', 'data': gameStateEvent diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts new file mode 100644 index 00000000..67c012f7 --- /dev/null +++ b/front/src/Api/iframe/player.ts @@ -0,0 +1,29 @@ +import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContribution"; +import type {HasPlayerMovedEvent, HasPlayerMovedEventCallback} from "../Events/HasPlayerMovedEvent"; +import {Subject} from "rxjs"; +import {apiCallback} from "./registeredCallbacks"; +import {isHasPlayerMovedEvent} from "../Events/HasPlayerMovedEvent"; + +const moveStream = new Subject(); + +class WorkadventurePlayerCommands extends IframeApiContribution { + callbacks = [ + apiCallback({ + type: 'hasPlayerMoved', + typeChecker: isHasPlayerMovedEvent, + callback: (payloadData) => { + moveStream.next(payloadData); + } + }), + ] + + onPlayerMove(callback: HasPlayerMovedEventCallback): void { + moveStream.subscribe(callback); + sendToWorkadventure({ + type: 'onPlayerMove', + data: null + }) + } +} + +export default new WorkadventurePlayerCommands(); \ No newline at end of file diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index ed412166..ea990d90 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -1,10 +1,54 @@ import { Subject } from "rxjs"; import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent'; -import { IframeApiContribution } from './IframeApiContribution'; +import {IframeApiContribution, sendToWorkadventure} from './IframeApiContribution'; import { apiCallback } from "./registeredCallbacks"; +import type {LayerEvent} from "../Events/LayerEvent"; +import type {SetPropertyEvent} from "../Events/setPropertyEvent"; +import type {GameStateEvent} from "../Events/GameStateEvent"; +import type {ITiledMap} from "../../Phaser/Map/ITiledMap"; +import type {DataLayerEvent} from "../Events/DataLayerEvent"; +import {isGameStateEvent} from "../Events/GameStateEvent"; +import {isDataLayerEvent} from "../Events/DataLayerEvent"; const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); +const dataLayerResolver = new Subject(); +const stateResolvers = new Subject(); + +let immutableData: GameStateEvent; + +interface Room { + id: string, + mapUrl: string, + map: ITiledMap, + startLayer: string | null +} + +interface User { + id: string | undefined, + nickName: string | null, + tags: string[] +} + + +function getGameState(): Promise { + if (immutableData) { + return Promise.resolve(immutableData); + } + else { + return new Promise((resolver, thrower) => { + stateResolvers.subscribe(resolver); + sendToWorkadventure({type: "getState", data: null}); + }) + } +} + +function getDataLayer(): Promise { + return new Promise((resolver, thrower) => { + dataLayerResolver.subscribe(resolver); + sendToWorkadventure({type: "getDataLayer", data: null}) + }) +} class WorkadventureRoomCommands extends IframeApiContribution { callbacks = [ @@ -21,8 +65,21 @@ class WorkadventureRoomCommands extends IframeApiContribution { leaveStreams.get(payloadData.name)?.next(); } - }) - + }), + apiCallback({ + type: "gameState", + typeChecker: isGameStateEvent, + callback: (payloadData) => { + stateResolvers.next(payloadData); + } + }), + apiCallback({ + type: "dataLayer", + typeChecker: isDataLayerEvent, + callback: (payloadData) => { + dataLayerResolver.next(payloadData); + } + }), ] @@ -43,6 +100,34 @@ class WorkadventureRoomCommands extends IframeApiContribution { + return getGameState().then((gameState) => { + return getDataLayer().then((mapJson) => { + return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName}; + }) + }) + } + getCurrentUser(): Promise { + return getGameState().then((gameState) => { + return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags}; + }) + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 8494503c..52d678ad 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -945,7 +945,7 @@ ${escapedMessage} })) this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { - iframeListener.sendFrozenGameStateEvent({ + iframeListener.sendGameStateEvent({ mapUrl: this.MapUrlFile, startLayerName: this.startLayerName, uuid: localUserStore.getLocalUser()?.uuid, diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 7b6b2db9..cc17fff2 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -12,6 +12,7 @@ import controls from "./Api/iframe/controls"; import ui from "./Api/iframe/ui"; import sound from "./Api/iframe/sound"; import room from "./Api/iframe/room"; +import player from "./Api/iframe/player"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Sound } from "./Api/iframe/Sound/Sound"; @@ -23,6 +24,7 @@ const wa = { chat, sound, room, + player, // All methods below are deprecated and should not be used anymore. // They are kept here for backward compatibility. diff --git a/maps/tests/Metadata/getCurrentRoom.html b/maps/tests/Metadata/getCurrentRoom.html index b290c6a4..7429b2a8 100644 --- a/maps/tests/Metadata/getCurrentRoom.html +++ b/maps/tests/Metadata/getCurrentRoom.html @@ -5,7 +5,7 @@ \ No newline at end of file diff --git a/maps/tests/Metadata/setProperty.html b/maps/tests/Metadata/setProperty.html index 06b029da..5259ec0a 100644 --- a/maps/tests/Metadata/setProperty.html +++ b/maps/tests/Metadata/setProperty.html @@ -5,8 +5,8 @@ \ No newline at end of file diff --git a/maps/tests/Metadata/showHideLayer.html b/maps/tests/Metadata/showHideLayer.html index 391ec449..4677f9e5 100644 --- a/maps/tests/Metadata/showHideLayer.html +++ b/maps/tests/Metadata/showHideLayer.html @@ -10,10 +10,10 @@ From a3eb715414ca760df81d05d7f4be9a5d33315292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 14:41:35 +0200 Subject: [PATCH 73/87] Improving wa-maps doc layout --- docs/maps/wa-maps.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/maps/wa-maps.md b/docs/maps/wa-maps.md index 98fdce2d..d7a349c5 100644 --- a/docs/maps/wa-maps.md +++ b/docs/maps/wa-maps.md @@ -60,21 +60,23 @@ By default, the characters can traverse any tiles. If you want to prevent your c To make a tile "collidable", you should: -1. select the relevant tileset and switch to "edit" mode: - {.document-img} - ![](https://workadventu.re/img/docs/collides-1.png) -2. right click on a tile of the tileset to select it: - {.document-img} - ![](https://workadventu.re/img/docs/collides-2.png) -3. on the left pane in the custom properties section, right click and select "Add properties": - {.document-img} - ![](https://workadventu.re/img/docs/collides-3.png) +1. select the relevant tileset and switch to "edit" mode: + + ![](https://workadventu.re/img/docs/collides-1.png){.document-img} + +2. right click on a tile of the tileset to select it: + + ![](https://workadventu.re/img/docs/collides-2.png){.document-img} + +3. on the left pane in the custom properties section, right click and select "Add properties": + + ![](https://workadventu.re/img/docs/collides-3.png){.document-img} Please add a `collides` property. The type of the property must be **bool**. -4. finally, check the checkbox for the `collides` property: - {.document-img} - ![](https://workadventu.re/img/docs/collides-4.png) +4. finally, check the checkbox for the `collides` property: + + ![](https://workadventu.re/img/docs/collides-4.png){.document-img} Repeat for every tile that should be "collidable". From 95d8cf92577e8ab847fa7873883243b96be922ec Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 23 Jun 2021 14:54:06 +0200 Subject: [PATCH 74/87] Change requested --- docs/maps/api-ui.md | 2 +- docs/maps/assets/menu-command.png | Bin 9856 -> 0 bytes front/src/Api/iframe/chat.ts | 2 +- front/src/Api/iframe/nav.ts | 8 ++++---- front/src/Api/iframe/room.ts | 6 +++--- front/src/Api/iframe/ui.ts | 2 +- front/src/WebRtc/MediaManager.ts | 8 ++++---- 7 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 docs/maps/assets/menu-command.png diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index fc38dc41..286f2ac7 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -85,5 +85,5 @@ WA.ui.registerMenuCommand("test", () => { ```
- +
\ No newline at end of file diff --git a/docs/maps/assets/menu-command.png b/docs/maps/assets/menu-command.png deleted file mode 100644 index 0caf75c9d772d63e43365040e033d3f93e69cf3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9856 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 Ho$&t!s2Avf diff --git a/front/src/Api/iframe/chat.ts b/front/src/Api/iframe/chat.ts index 5f73b744..7d8e6f71 100644 --- a/front/src/Api/iframe/chat.ts +++ b/front/src/Api/iframe/chat.ts @@ -23,7 +23,7 @@ class WorkadventureChatCommands extends IframeApiContribution { diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index 8e9943b2..c7655b84 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -90,7 +90,7 @@ class WorkAdventureUiCommands extends IframeApiContribution { - const elementChildre = element.children.item(index); - if (!elementChildre) { + const elementChildren = element.children.item(index); + if (!elementChildren) { return; } - elementChildre.classList.remove('active'); + elementChildren.classList.remove('active'); if ((index + 1) > volume) { return; } - elementChildre.classList.add('active'); + elementChildren.classList.add('active'); }); } From 623767285575033df54673b2ff8be6bbef81f129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 15:26:17 +0200 Subject: [PATCH 75/87] Create SECURITY.md --- SECURITY.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..562da3e0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Reporting a Vulnerability + +First things first: **Do NOT report security vulnerabilities in public issues!** + +Please disclose responsibly by sending +a mail at security@workadventu.re (you can also ping us in the GitHub issues, but please, no details in the issues!) + +We will assess the issue as soon as possible on a best-effort basis and will give you an estimate for when we have a fix +and release available for an eventual public disclosure. + +We do not have a bug bounty program. + +## Supported Versions + +We only apply security patches on the latest tagged release and on the `master` and `develop` branches + +Unless specified otherwise, do not expect us to fix security issues on past releases. We are only maintaining one release: +the latest one, which is online at https://play.workadventu.re. From a5f0cadccd6830eb36f4b01e0c508617a2e17a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 15:29:02 +0200 Subject: [PATCH 76/87] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..3a2a562d --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ develop ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] + schedule: + - cron: '24 17 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From c0bfec7b11e26c90674b9b59808f3b81a16978e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 15:51:31 +0200 Subject: [PATCH 77/87] Adding Prettier configuration --- front/.prettierignore | 1 + front/.prettierrc.json | 3 +++ front/package.json | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 front/.prettierignore create mode 100644 front/.prettierrc.json diff --git a/front/.prettierignore b/front/.prettierignore new file mode 100644 index 00000000..d7d4e5c0 --- /dev/null +++ b/front/.prettierignore @@ -0,0 +1 @@ +src/Messages diff --git a/front/.prettierrc.json b/front/.prettierrc.json new file mode 100644 index 00000000..5fcd8a70 --- /dev/null +++ b/front/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/front/package.json b/front/package.json index e2702c28..6c874ec7 100644 --- a/front/package.json +++ b/front/package.json @@ -64,7 +64,9 @@ "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "precommit": "lint-staged", "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch", - "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"" + "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"", + "pretty": "yarn prettier --write src/", + "pretty-check": "yarn prettier --check src/" }, "lint-staged": { "*.ts": [ From d2be9fa931d777894ea35abe89ccbd87521daef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 15:54:13 +0200 Subject: [PATCH 78/87] Adding contributing guide explaining how to setup the precommit hook --- CONTRIBUTING.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e5cd50dc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing to WorkAdventure + +Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to +ask questions and how to work on something. + +## Contributions we are seeking + +We love to receive contributions from our community — you! + +There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, +submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself. + +## Using the issue tracker + +First things first: **Do NOT report security vulnerabilities in public issues!**. +Please read the [security guide](SECURITY.md) to learn who to do a security disclosure to the WorkAdventure core team. + +You can use [GitHub issue tracker](https://github.com/thecodingmachine/workadventure/issues) to: + +- File bug reports +- Ask for feature requests + +If you have more general questions, a good place to ask is [our Discord server](https://discord.gg/YGtngdh9gt). + +Finally, you can come and talk to the WorkAdventure core team... on WorkAdventure, of course! [Our offices are here](https://play.staging.workadventu.re/@/tcm/workadventure/wa-village). + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope +and avoid containing unrelated commits. + +Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), +otherwise you risk spending a lot of time working on something that the project's developers might not want to merge +into the project. + +You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues). + +### Linting your code + +Before committing, be sure to install the "Prettier" precommit hook that will reformat your code to our coding style. + +In order to enable the "Prettier" precommit hook, at the root of the project, run: + +```console +$ yarn run install +$ yarn run prepare +``` + +### Providing tests + +WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test. + +Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine). + +If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain +some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file +to add a reference to your newly created test map. + From 4cac260f813e5bd340bf6eb229661f7cf2258380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 16:23:16 +0200 Subject: [PATCH 79/87] Adding prettier on back and pusher (not running them right now) --- back/.prettierignore | 1 + back/.prettierrc.json | 3 +++ back/package.json | 5 ++++- back/yarn.lock | 5 +++++ front/.prettierignore | 2 +- pusher/.prettierignore | 1 + pusher/.prettierrc.json | 3 +++ pusher/package.json | 5 ++++- pusher/yarn.lock | 5 +++++ 9 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 back/.prettierignore create mode 100644 back/.prettierrc.json create mode 100644 pusher/.prettierignore create mode 100644 pusher/.prettierrc.json diff --git a/back/.prettierignore b/back/.prettierignore new file mode 100644 index 00000000..1f453464 --- /dev/null +++ b/back/.prettierignore @@ -0,0 +1 @@ +src/Messages/generated diff --git a/back/.prettierrc.json b/back/.prettierrc.json new file mode 100644 index 00000000..5fcd8a70 --- /dev/null +++ b/back/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/back/package.json b/back/package.json index a6ff7319..5df9e48e 100644 --- a/back/package.json +++ b/back/package.json @@ -11,7 +11,9 @@ "profile": "tsc && node --prof ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "node_modules/.bin/eslint src/ . --ext .ts", - "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts" + "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", + "pretty": "yarn prettier --write src/", + "pretty-check": "yarn prettier --check src/" }, "repository": { "type": "git", @@ -66,6 +68,7 @@ "@typescript-eslint/parser": "^2.26.0", "eslint": "^6.8.0", "jasmine": "^3.5.0", + "prettier": "^2.3.1", "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3" } diff --git a/back/yarn.lock b/back/yarn.lock index 3ac4b0a8..813e0f8e 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -2039,6 +2039,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" diff --git a/front/.prettierignore b/front/.prettierignore index d7d4e5c0..1f453464 100644 --- a/front/.prettierignore +++ b/front/.prettierignore @@ -1 +1 @@ -src/Messages +src/Messages/generated diff --git a/pusher/.prettierignore b/pusher/.prettierignore new file mode 100644 index 00000000..1f453464 --- /dev/null +++ b/pusher/.prettierignore @@ -0,0 +1 @@ +src/Messages/generated diff --git a/pusher/.prettierrc.json b/pusher/.prettierrc.json new file mode 100644 index 00000000..5fcd8a70 --- /dev/null +++ b/pusher/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/pusher/package.json b/pusher/package.json index 63188032..3417f2c1 100644 --- a/pusher/package.json +++ b/pusher/package.json @@ -11,7 +11,9 @@ "profile": "tsc && node --prof ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", - "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts" + "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", + "pretty": "yarn prettier --write src/", + "pretty-check": "yarn prettier --check src/" }, "repository": { "type": "git", @@ -65,6 +67,7 @@ "@typescript-eslint/parser": "^2.26.0", "eslint": "^6.8.0", "jasmine": "^3.5.0", + "prettier": "^2.3.1", "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3" } diff --git a/pusher/yarn.lock b/pusher/yarn.lock index 88a20475..6657c0a8 100644 --- a/pusher/yarn.lock +++ b/pusher/yarn.lock @@ -2039,6 +2039,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" From 9f9cf5a3bba3e419e66b6f380ba2a2ff54da33db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 16:30:42 +0200 Subject: [PATCH 80/87] Fixing .gitignore files --- .gitignore | 3 ++- front/.gitignore | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 70660058..2cbf66b1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ docker-compose.override.yaml *.DS_Store maps/yarn.lock maps/dist/computer.js -maps/dist/computer.js.map \ No newline at end of file +maps/dist/computer.js.map +/node_modules/ diff --git a/front/.gitignore b/front/.gitignore index b6f01e2c..1842041a 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -1,5 +1,9 @@ /node_modules/ -/dist/bundle.js +/dist/*.js +/dist/*.js.map +/dist/*.js.LICENSE.txt +/dist/main.*.css +/dist/main.*.css.map /dist/tests/ /yarn-error.log /dist/webpack.config.js From 3eab074e2a8f991bddf5bf52da04f2350c6694e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 16:45:38 +0200 Subject: [PATCH 81/87] Setting up lint-staged on front and back --- .husky/pre-commit | 13 +- back/package.json | 8 + back/yarn.lock | 399 +++++++++++++++++++++++++++++++++++++++++++- pusher/package.json | 8 + pusher/yarn.lock | 399 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 816 insertions(+), 11 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index e43d66b5..5944be37 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,15 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -cd front && yarn run precommit +( +cd front || exit +yarn run precommit +) +( +cd pusher || exit +yarn run precommit +) +( +cd back || exit +yarn run precommit +) diff --git a/back/package.json b/back/package.json index 5df9e48e..48c496a0 100644 --- a/back/package.json +++ b/back/package.json @@ -12,6 +12,7 @@ "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", + "precommit": "lint-staged", "pretty": "yarn prettier --write src/", "pretty-check": "yarn prettier --check src/" }, @@ -68,8 +69,15 @@ "@typescript-eslint/parser": "^2.26.0", "eslint": "^6.8.0", "jasmine": "^3.5.0", + "lint-staged": "^11.0.0", "prettier": "^2.3.1", "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3" + }, + "lint-staged": { + "*.ts": [ + "prettier --write", + "git add" + ] } } diff --git a/back/yarn.lock b/back/yarn.lock index 813e0f8e..242728db 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -117,6 +117,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" @@ -197,6 +202,14 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv@^6.10.0, ajv@^6.10.2: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" @@ -207,6 +220,11 @@ ajv@^6.10.0, ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-escapes@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" @@ -214,6 +232,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.11.0" +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -241,6 +266,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -325,6 +357,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -389,7 +426,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -475,6 +512,14 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -515,6 +560,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -522,6 +572,14 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -573,11 +631,21 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + colour@~0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -603,6 +671,17 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -614,6 +693,15 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -667,6 +755,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -752,7 +845,14 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -error-ex@^1.2.0: +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -877,6 +977,21 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -1066,11 +1181,21 @@ generic-type-guard@^3.2.0: resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1" integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ== +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -1193,6 +1318,11 @@ http-status-codes@*: resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1220,6 +1350,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -1232,6 +1370,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1402,6 +1545,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -1409,6 +1557,21 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -1467,6 +1630,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1549,6 +1717,45 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +lint-staged@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" + integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + dedent "^0.7.0" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -1610,6 +1817,24 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + long@~3: version "3.2.0" resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" @@ -1661,6 +1886,11 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.2.3: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1685,6 +1915,14 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -1853,6 +2091,13 @@ npm-packlist@^1.1.6: npm-bundled "^1.0.1" npm-normalize-package-bin "^1.0.1" +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -1903,7 +2148,7 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^5.1.0: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -1952,6 +2197,13 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -1966,6 +2218,16 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -1993,6 +2255,11 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -2007,11 +2274,21 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2029,6 +2306,13 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -2231,6 +2515,13 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2258,6 +2549,11 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -2295,12 +2591,24 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -signal-exit@^3.0.0, signal-exit@^3.0.2: +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -2314,6 +2622,24 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -2439,6 +2765,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -2474,6 +2805,15 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -2481,6 +2821,15 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -2521,6 +2870,11 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -2592,7 +2946,7 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@^2.3.6: +through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -2708,6 +3062,11 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -2795,6 +3154,13 @@ which@^1.2.9: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -2820,6 +3186,24 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -2847,6 +3231,11 @@ yallist@^3.0.0, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs@^3.10.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" diff --git a/pusher/package.json b/pusher/package.json index 3417f2c1..6d13e911 100644 --- a/pusher/package.json +++ b/pusher/package.json @@ -12,6 +12,7 @@ "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", + "precommit": "lint-staged", "pretty": "yarn prettier --write src/", "pretty-check": "yarn prettier --check src/" }, @@ -67,8 +68,15 @@ "@typescript-eslint/parser": "^2.26.0", "eslint": "^6.8.0", "jasmine": "^3.5.0", + "lint-staged": "^11.0.0", "prettier": "^2.3.1", "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3" + }, + "lint-staged": { + "*.ts": [ + "prettier --write", + "git add" + ] } } diff --git a/pusher/yarn.lock b/pusher/yarn.lock index 6657c0a8..f078bddf 100644 --- a/pusher/yarn.lock +++ b/pusher/yarn.lock @@ -117,6 +117,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" @@ -197,6 +202,14 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv@^6.10.0, ajv@^6.10.2: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" @@ -207,6 +220,11 @@ ajv@^6.10.0, ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-escapes@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" @@ -214,6 +232,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.11.0" +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -241,6 +266,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -325,6 +357,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -389,7 +426,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -475,6 +512,14 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -515,6 +560,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -522,6 +572,14 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -573,11 +631,21 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + colour@~0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -603,6 +671,17 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -614,6 +693,15 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -667,6 +755,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -752,7 +845,14 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -error-ex@^1.2.0: +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -877,6 +977,21 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -1066,11 +1181,21 @@ generic-type-guard@^3.2.0: resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1" integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ== +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -1193,6 +1318,11 @@ http-status-codes@*: resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1220,6 +1350,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -1232,6 +1370,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1402,6 +1545,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -1409,6 +1557,21 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -1467,6 +1630,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1549,6 +1717,45 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +lint-staged@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" + integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + dedent "^0.7.0" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -1610,6 +1817,24 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + long@~3: version "3.2.0" resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" @@ -1661,6 +1886,11 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.2.3: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1685,6 +1915,14 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -1853,6 +2091,13 @@ npm-packlist@^1.1.6: npm-bundled "^1.0.1" npm-normalize-package-bin "^1.0.1" +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -1903,7 +2148,7 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^5.1.0: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -1952,6 +2197,13 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -1966,6 +2218,16 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -1993,6 +2255,11 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -2007,11 +2274,21 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2029,6 +2306,13 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -2231,6 +2515,13 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2258,6 +2549,11 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -2295,12 +2591,24 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -signal-exit@^3.0.0, signal-exit@^3.0.2: +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -2314,6 +2622,24 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -2439,6 +2765,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -2474,6 +2805,15 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -2481,6 +2821,15 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -2521,6 +2870,11 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -2587,7 +2941,7 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@^2.3.6: +through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -2703,6 +3057,11 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -2790,6 +3149,13 @@ which@^1.2.9: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -2815,6 +3181,24 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -2842,6 +3226,11 @@ yallist@^3.0.0, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs@^3.10.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" From f6d3783f6c15c5d0752b139462a7a7cdf497218c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 16:50:00 +0200 Subject: [PATCH 82/87] Removing git add from lint-staged since it is not necessary since v10 --- back/package.json | 3 +-- front/package.json | 3 +-- pusher/package.json | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/back/package.json b/back/package.json index 48c496a0..1fa43998 100644 --- a/back/package.json +++ b/back/package.json @@ -76,8 +76,7 @@ }, "lint-staged": { "*.ts": [ - "prettier --write", - "git add" + "prettier --write" ] } } diff --git a/front/package.json b/front/package.json index 541617cb..def0958e 100644 --- a/front/package.json +++ b/front/package.json @@ -71,8 +71,7 @@ }, "lint-staged": { "*.ts": [ - "prettier --write", - "git add" + "prettier --write" ] } } diff --git a/pusher/package.json b/pusher/package.json index 6d13e911..3140f635 100644 --- a/pusher/package.json +++ b/pusher/package.json @@ -75,8 +75,7 @@ }, "lint-staged": { "*.ts": [ - "prettier --write", - "git add" + "prettier --write" ] } } From 3f0ef2d31a555beb8f4e9b8ad4cf2abebdbad390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 16:56:33 +0200 Subject: [PATCH 83/87] Fixing prettier run command --- back/package.json | 4 ++-- front/package.json | 4 ++-- pusher/package.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/back/package.json b/back/package.json index 1fa43998..5bf5d031 100644 --- a/back/package.json +++ b/back/package.json @@ -13,8 +13,8 @@ "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "precommit": "lint-staged", - "pretty": "yarn prettier --write src/", - "pretty-check": "yarn prettier --check src/" + "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", + "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" }, "repository": { "type": "git", diff --git a/front/package.json b/front/package.json index def0958e..61205855 100644 --- a/front/package.json +++ b/front/package.json @@ -66,8 +66,8 @@ "precommit": "lint-staged", "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch", "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"", - "pretty": "yarn prettier --write src/", - "pretty-check": "yarn prettier --check src/" + "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", + "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" }, "lint-staged": { "*.ts": [ diff --git a/pusher/package.json b/pusher/package.json index 3140f635..8ad227ae 100644 --- a/pusher/package.json +++ b/pusher/package.json @@ -13,8 +13,8 @@ "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", "precommit": "lint-staged", - "pretty": "yarn prettier --write src/", - "pretty-check": "yarn prettier --check src/" + "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", + "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" }, "repository": { "type": "git", From 0828dd8af3c811b802633bb7346b9997930c291f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 16:58:57 +0200 Subject: [PATCH 84/87] Making max line length to 120 --- back/.prettierrc.json | 1 + front/.prettierrc.json | 1 + pusher/.prettierrc.json | 1 + 3 files changed, 3 insertions(+) diff --git a/back/.prettierrc.json b/back/.prettierrc.json index 5fcd8a70..e8980d15 100644 --- a/back/.prettierrc.json +++ b/back/.prettierrc.json @@ -1,3 +1,4 @@ { + "printWidth": 120, "tabWidth": 4 } diff --git a/front/.prettierrc.json b/front/.prettierrc.json index 5fcd8a70..e8980d15 100644 --- a/front/.prettierrc.json +++ b/front/.prettierrc.json @@ -1,3 +1,4 @@ { + "printWidth": 120, "tabWidth": 4 } diff --git a/pusher/.prettierrc.json b/pusher/.prettierrc.json index 5fcd8a70..e8980d15 100644 --- a/pusher/.prettierrc.json +++ b/pusher/.prettierrc.json @@ -1,3 +1,4 @@ { + "printWidth": 120, "tabWidth": 4 } From 06b7f5ba2f09937c17e09e8721de3f660e40a866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 23 Jun 2021 17:13:16 +0200 Subject: [PATCH 85/87] Applying prettier in CI for pusher and back containers --- .github/workflows/continuous_integration.yml | 13 +++++++++++++ CONTRIBUTING.md | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 2036e4e6..faf50c7a 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -64,6 +64,11 @@ jobs: run: yarn test working-directory: "front" + # We will enable prettier checks on front in a few month, when most PRs without prettier have been merged + # - name: "Prettier" + # run: yarn run pretty-check + # working-directory: "front" + continuous-integration-pusher: name: "Continuous Integration Pusher" @@ -107,6 +112,10 @@ jobs: run: yarn test working-directory: "pusher" + - name: "Prettier" + run: yarn run pretty-check + working-directory: "pusher" + continuous-integration-back: name: "Continuous Integration Back" @@ -150,3 +159,7 @@ jobs: run: yarn test working-directory: "back" + - name: "Prettier" + run: yarn run pretty-check + working-directory: "back" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5cd50dc..b85d0a98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,15 @@ $ yarn run install $ yarn run prepare ``` +If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need +to run code linting manually: + +```console +$ docker-compose exec front yarn run pretty +$ docker-compose exec pusher yarn run pretty +$ docker-compose exec back yarn run pretty +``` + ### Providing tests WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test. From 10c3d6dee2a30e1ee6f6299352c7593cfbd26d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 24 Jun 2021 10:09:10 +0200 Subject: [PATCH 86/87] Applying Prettier on pusher and back --- back/src/App.ts | 6 +- back/src/Controller/BaseController.ts | 9 +- back/src/Controller/DebugController.ts | 67 ++-- back/src/Controller/PrometheusController.ts | 10 +- back/src/Enum/EnvironmentVariable.ts | 24 +- back/src/Model/Admin.ts | 13 +- back/src/Model/GameRoom.ts | 122 +++---- back/src/Model/Group.ts | 39 +-- back/src/Model/Movable.ts | 4 +- back/src/Model/PositionInterface.ts | 4 +- back/src/Model/PositionNotifier.ts | 26 +- back/src/Model/RoomIdentifier.ts | 24 +- back/src/Model/User.ts | 23 +- back/src/Model/Websocket/CharacterLayer.ts | 4 +- back/src/Model/Websocket/ItemEventMessage.ts | 7 +- .../Model/Websocket/MessageUserPosition.ts | 13 +- back/src/Model/Websocket/PointInterface.ts | 9 +- back/src/Model/Websocket/ProtobufUtils.ts | 35 +- back/src/Model/Zone.ts | 57 ++-- back/src/RoomManager.ts | 165 ++++++---- back/src/Server/server/app.ts | 16 +- back/src/Server/server/baseapp.ts | 173 +++++----- back/src/Server/server/formdata.ts | 167 +++++----- back/src/Server/server/sslapp.ts | 16 +- back/src/Server/server/types.ts | 8 +- back/src/Server/server/utils.ts | 55 ++-- back/src/Server/sifrr.server.ts | 22 +- back/src/Services/ArrayHelper.ts | 6 +- back/src/Services/ClientEventsEmitter.ts | 6 +- back/src/Services/CpuTracker.ts | 18 +- back/src/Services/GaugeManager.ts | 40 +-- back/src/Services/MessageHelpers.ts | 6 +- back/src/Services/SocketManager.ts | 277 +++++++++------- pusher/src/App.ts | 14 +- pusher/src/Controller/AdminController.ts | 114 ++++--- .../src/Controller/AuthenticateController.ts | 96 +++--- pusher/src/Controller/BaseController.ts | 23 +- pusher/src/Controller/DebugController.ts | 53 +-- pusher/src/Controller/IoSocketController.ts | 264 ++++++++------- pusher/src/Controller/MapController.ts | 39 ++- pusher/src/Controller/PrometheusController.ts | 10 +- pusher/src/Enum/EnvironmentVariable.ts | 22 +- pusher/src/Model/Movable.ts | 4 +- pusher/src/Model/PositionDispatcher.ts | 23 +- pusher/src/Model/PositionInterface.ts | 4 +- pusher/src/Model/PusherRoom.ts | 26 +- pusher/src/Model/RoomIdentifier.ts | 24 +- .../Model/Websocket/ExAdminSocketInterface.ts | 23 +- .../src/Model/Websocket/ExSocketInterface.ts | 34 +- .../src/Model/Websocket/ItemEventMessage.ts | 7 +- pusher/src/Model/Websocket/Point.ts | 12 +- pusher/src/Model/Websocket/PointInterface.ts | 9 +- pusher/src/Model/Websocket/ProtobufUtils.ts | 33 +- pusher/src/Model/Websocket/ViewportMessage.ts | 7 +- pusher/src/Model/Zone.ts | 96 ++++-- pusher/src/Server/server/app.ts | 16 +- pusher/src/Server/server/baseapp.ts | 173 +++++----- pusher/src/Server/server/formdata.ts | 167 +++++----- pusher/src/Server/server/sslapp.ts | 16 +- pusher/src/Server/server/types.ts | 8 +- pusher/src/Server/server/utils.ts | 55 ++-- pusher/src/Server/sifrr.server.ts | 22 +- pusher/src/Services/AdminApi.ts | 132 +++++--- pusher/src/Services/ApiClientRepository.ts | 23 +- pusher/src/Services/ArrayHelper.ts | 6 +- pusher/src/Services/ClientEventsEmitter.ts | 10 +- pusher/src/Services/CpuTracker.ts | 18 +- pusher/src/Services/GaugeManager.ts | 36 +- pusher/src/Services/IoSocketHelpers.ts | 7 +- pusher/src/Services/JWTTokenManager.ts | 96 +++--- pusher/src/Services/SocketManager.ts | 307 +++++++++--------- 71 files changed, 1848 insertions(+), 1652 deletions(-) diff --git a/back/src/App.ts b/back/src/App.ts index 4bcc56ba..a6c42abb 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -1,7 +1,7 @@ // lib/app.ts -import {PrometheusController} from "./Controller/PrometheusController"; -import {DebugController} from "./Controller/DebugController"; -import {App as uwsApp} from "./Server/sifrr.server"; +import { PrometheusController } from "./Controller/PrometheusController"; +import { DebugController } from "./Controller/DebugController"; +import { App as uwsApp } from "./Server/sifrr.server"; class App { public app: uwsApp; diff --git a/back/src/Controller/BaseController.ts b/back/src/Controller/BaseController.ts index 93c17ab4..dc510d6c 100644 --- a/back/src/Controller/BaseController.ts +++ b/back/src/Controller/BaseController.ts @@ -1,10 +1,9 @@ -import {HttpResponse} from "uWebSockets.js"; - +import { HttpResponse } from "uWebSockets.js"; export class BaseController { protected addCorsHeaders(res: HttpResponse): void { - res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept'); - res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); - res.writeHeader('access-control-allow-origin', '*'); + res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept"); + res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE"); + res.writeHeader("access-control-allow-origin", "*"); } } diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index 509d8b2f..b7f037fd 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -1,53 +1,54 @@ -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {stringify} from "circular-json"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -import { parse } from 'query-string'; -import {App} from "../Server/sifrr.server"; -import {socketManager} from "../Services/SocketManager"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { stringify } from "circular-json"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +import { parse } from "query-string"; +import { App } from "../Server/sifrr.server"; +import { socketManager } from "../Services/SocketManager"; export class DebugController { - constructor(private App : App) { + constructor(private App: App) { this.getDump(); } - - getDump(){ + getDump() { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { const query = parse(req.getQuery()); if (query.token !== ADMIN_API_TOKEN) { - return res.status(401).send('Invalid token sent!'); + return res.status(401).send("Invalid token sent!"); } - return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( - socketManager.getWorlds(), - (key: unknown, value: unknown) => { - if (key === 'listeners') { - return 'Listeners'; - } - if (key === 'socket') { - return 'Socket'; - } - if (key === 'batchedMessages') { - return 'BatchedMessages'; - } - if(value instanceof Map) { - const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any - for (const [mapKey, mapValue] of value.entries()) { - obj[mapKey] = mapValue; + return res + .writeStatus("200 OK") + .writeHeader("Content-Type", "application/json") + .end( + stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { + if (key === "listeners") { + return "Listeners"; } - return obj; - } else if(value instanceof Set) { + if (key === "socket") { + return "Socket"; + } + if (key === "batchedMessages") { + return "BatchedMessages"; + } + if (value instanceof Map) { + const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any + for (const [mapKey, mapValue] of value.entries()) { + obj[mapKey] = mapValue; + } + return obj; + } else if (value instanceof Set) { const obj: Array = []; for (const [setKey, setValue] of value.entries()) { obj.push(setValue); } return obj; - } else { - return value; - } - } - )); + } else { + return value; + } + }) + ); }); } } diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts index e854cf43..3ab3d33f 100644 --- a/back/src/Controller/PrometheusController.ts +++ b/back/src/Controller/PrometheusController.ts @@ -1,7 +1,7 @@ -import {App} from "../Server/sifrr.server"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -const register = require('prom-client').register; -const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; +import { App } from "../Server/sifrr.server"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +const register = require("prom-client").register; +const collectDefaultMetrics = require("prom-client").collectDefaultMetrics; export class PrometheusController { constructor(private App: App) { @@ -14,7 +14,7 @@ export class PrometheusController { } private metrics(res: HttpResponse, req: HttpRequest): void { - res.writeHeader('Content-Type', register.contentType); + res.writeHeader("Content-Type", register.contentType); res.end(register.metrics()); } } diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 81693a98..19eddd3e 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -1,17 +1,17 @@ const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; -const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; -const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; +const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; +const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; -const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; -const JITSI_ISS = process.env.JITSI_ISS || ''; -const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; -const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080; -const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051; +const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; +const JITSI_ISS = process.env.JITSI_ISS || ""; +const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; +const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080; +const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051; export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed -export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ''; -export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4'); +export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ""; +export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); export { MINIMUM_DISTANCE, @@ -24,5 +24,5 @@ export { CPU_OVERHEAT_THRESHOLD, JITSI_URL, JITSI_ISS, - SECRET_JITSI_KEY -} + SECRET_JITSI_KEY, +}; diff --git a/back/src/Model/Admin.ts b/back/src/Model/Admin.ts index 29b53385..93396fa8 100644 --- a/back/src/Model/Admin.ts +++ b/back/src/Model/Admin.ts @@ -1,15 +1,12 @@ import { ServerToAdminClientMessage, - UserJoinedRoomMessage, UserLeftRoomMessage + UserJoinedRoomMessage, + UserLeftRoomMessage, } from "../Messages/generated/messages_pb"; -import {AdminSocket} from "../RoomManager"; - +import { AdminSocket } from "../RoomManager"; export class Admin { - public constructor( - private readonly socket: AdminSocket - ) { - } + public constructor(private readonly socket: AdminSocket) {} public sendUserJoin(uuid: string, name: string, ip: string): void { const serverToAdminClientMessage = new ServerToAdminClientMessage(); @@ -24,7 +21,7 @@ export class Admin { this.socket.write(serverToAdminClientMessage); } - public sendUserLeft(uuid: string/*, name: string, ip: string*/): void { + public sendUserLeft(uuid: string /*, name: string, ip: string*/): void { const serverToAdminClientMessage = new ServerToAdminClientMessage(); const userLeftRoomMessage = new UserLeftRoomMessage(); diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 53d0a855..020f4c29 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -1,16 +1,16 @@ -import {PointInterface} from "./Websocket/PointInterface"; -import {Group} from "./Group"; -import {User, UserSocket} from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; -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 {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {ZoneSocket} from "src/RoomManager"; -import {Admin} from "../Model/Admin"; +import { PointInterface } from "./Websocket/PointInterface"; +import { Group } from "./Group"; +import { User, UserSocket } from "./User"; +import { PositionInterface } from "_Model/PositionInterface"; +import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone"; +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 { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { ZoneSocket } from "src/RoomManager"; +import { Admin } from "../Model/Admin"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; @@ -39,33 +39,33 @@ export class GameRoom { private readonly positionNotifier: PositionNotifier; public readonly roomId: string; public readonly roomSlug: string; - public readonly worldSlug: string = ''; - public readonly organizationSlug: string = ''; - private versionNumber:number = 1; + public readonly worldSlug: string = ""; + public readonly organizationSlug: string = ""; + private versionNumber: number = 1; private nextUserId: number = 1; - constructor(roomId: string, - connectCallback: ConnectCallback, - disconnectCallback: DisconnectCallback, - minDistance: number, - groupRadius: number, - onEnters: EntersCallback, - onMoves: MovesCallback, - onLeaves: LeavesCallback, - onEmote: EmoteCallback, + constructor( + roomId: string, + connectCallback: ConnectCallback, + disconnectCallback: DisconnectCallback, + minDistance: number, + groupRadius: number, + onEnters: EntersCallback, + onMoves: MovesCallback, + onLeaves: LeavesCallback, + onEmote: EmoteCallback ) { this.roomId = roomId; if (isRoomAnonymous(roomId)) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { - const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId); this.roomSlug = roomSlug; this.organizationSlug = organizationSlug; this.worldSlug = worldSlug; } - this.users = new Map(); this.usersByUuid = new Map(); this.admins = new Set(); @@ -86,21 +86,22 @@ export class GameRoom { return this.users; } - public getUserByUuid(uuid: string): User|undefined { + public getUserByUuid(uuid: string): User | undefined { return this.usersByUuid.get(uuid); } - public getUserById(id: number): User|undefined { + public getUserById(id: number): User | undefined { return this.users.get(id); } - - public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User { + + public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User { const positionMessage = joinRoomMessage.getPositionmessage(); if (positionMessage === undefined) { - throw new Error('Missing position message'); + throw new Error("Missing position message"); } const position = ProtobufUtils.toPointInterface(positionMessage); - const user = new User(this.nextUserId, + const user = new User( + this.nextUserId, joinRoomMessage.getUseruuid(), joinRoomMessage.getIpaddress(), position, @@ -126,12 +127,12 @@ export class GameRoom { return user; } - public leave(user : User){ + public leave(user: User) { const userObj = this.users.get(user.id); if (userObj === undefined) { - console.warn('User ', user.id, 'does not belong to this game room! It should!'); + console.warn("User ", user.id, "does not belong to this game room! It should!"); } - if (userObj !== undefined && typeof userObj.group !== 'undefined') { + if (userObj !== undefined && typeof userObj.group !== "undefined") { this.leaveGroup(userObj); } this.users.delete(user.id); @@ -143,7 +144,7 @@ export class GameRoom { // Notify admins for (const admin of this.admins) { - admin.sendUserLeft(user.uuid/*, user.name, user.IPAddress*/); + admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/); } } @@ -151,7 +152,7 @@ export class GameRoom { return this.users.size === 0 && this.admins.size === 0; } - public updatePosition(user : User, userPosition: PointInterface): void { + public updatePosition(user: User, userPosition: PointInterface): void { user.setPosition(userPosition); this.updateUserGroup(user); @@ -173,22 +174,24 @@ export class GameRoom { return; } - const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user); + const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user); if (closestItem !== null) { if (closestItem instanceof Group) { // Let's join the group! closestItem.join(user); } else { - const closestUser : User = closestItem; - const group: Group = new Group(this.roomId,[ - user, - closestUser - ], this.connectCallback, this.disconnectCallback, this.positionNotifier); + const closestUser: User = closestItem; + const group: Group = new Group( + this.roomId, + [user, closestUser], + this.connectCallback, + this.disconnectCallback, + this.positionNotifier + ); this.groups.add(group); } } - } else { // If the user is part of a group: // should he leave the group? @@ -229,7 +232,9 @@ export class GameRoom { this.positionNotifier.leave(group); group.destroy(); if (!this.groups.has(group)) { - throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World."); + throw new Error( + "Could not find group " + group.getId() + " referenced by user " + user.id + " in World." + ); } this.groups.delete(group); //todo: is the group garbage collected? @@ -247,16 +252,15 @@ export class GameRoom { * OR * - close enough to a group (distance <= groupRadius) */ - private searchClosestAvailableUserOrGroup(user: User): User|Group|null - { + private searchClosestAvailableUserOrGroup(user: User): User | Group | null { let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius); let matchingItem: User | Group | null = null; this.users.forEach((currentUser, userId) => { // Let's only check users that are not part of a group - if (typeof currentUser.group !== 'undefined') { + if (typeof currentUser.group !== "undefined") { return; } - if(currentUser === user) { + if (currentUser === user) { return; } if (currentUser.silent) { @@ -265,7 +269,7 @@ export class GameRoom { const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. - if(distance <= minimumDistanceFound && distance <= this.minDistance) { + if (distance <= minimumDistanceFound && distance <= this.minDistance) { minimumDistanceFound = distance; matchingItem = currentUser; } @@ -276,7 +280,7 @@ export class GameRoom { return; } const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); - if(distance <= minimumDistanceFound && distance <= this.groupRadius) { + if (distance <= minimumDistanceFound && distance <= this.groupRadius) { minimumDistanceFound = distance; matchingItem = group; } @@ -285,15 +289,15 @@ export class GameRoom { return matchingItem; } - public static computeDistance(user1: User, user2: User): number - { + public static computeDistance(user1: User, user2: User): number { const user1Position = user1.getPosition(); const user2Position = user2.getPosition(); - return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)); + return Math.sqrt( + Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2) + ); } - public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number - { + public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number { return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); } @@ -325,9 +329,9 @@ export class GameRoom { public adminLeave(admin: Admin): void { this.admins.delete(admin); } - + public incrementVersion(): number { - this.versionNumber++ + this.versionNumber++; return this.versionNumber; } diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index ffe7a78a..5a0f3be6 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,13 +1,12 @@ import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { User } from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {gaugeManager} from "../Services/GaugeManager"; -import {MAX_PER_GROUP} from "../Enum/EnvironmentVariable"; +import { PositionInterface } from "_Model/PositionInterface"; +import { Movable } from "_Model/Movable"; +import { PositionNotifier } from "_Model/PositionNotifier"; +import { gaugeManager } from "../Services/GaugeManager"; +import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable"; export class Group implements Movable { - private static nextId: number = 1; private id: number; @@ -18,8 +17,13 @@ export class Group implements Movable { private wasDestroyed: boolean = false; private roomId: string; - - constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { + constructor( + roomId: string, + users: User[], + private connectCallback: ConnectCallback, + private disconnectCallback: DisconnectCallback, + private positionNotifier: PositionNotifier + ) { this.roomId = roomId; this.users = new Set(); this.id = Group.nextId; @@ -43,7 +47,7 @@ export class Group implements Movable { return Array.from(this.users.values()); } - getId() : number { + getId(): number { return this.id; } @@ -53,7 +57,7 @@ export class Group implements Movable { getPosition(): PositionInterface { return { x: this.x, - y: this.y + y: this.y, }; } @@ -83,7 +87,7 @@ export class Group implements Movable { if (oldX === undefined) { this.positionNotifier.enter(this); } else { - this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY}); + this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY }); } } @@ -95,19 +99,17 @@ export class Group implements Movable { return this.users.size <= 1; } - join(user: User): void - { + join(user: User): void { // Broadcast on the right event this.connectCallback(user, this); this.users.add(user); user.group = this; } - leave(user: User): void - { + leave(user: User): void { const success = this.users.delete(user); if (success === false) { - throw new Error("Could not find user "+user.id+" in the group "+this.id); + throw new Error("Could not find user " + user.id + " in the group " + this.id); } user.group = undefined; @@ -123,8 +125,7 @@ export class Group implements Movable { * Let's kick everybody out. * Usually used when there is only one user left. */ - destroy(): void - { + destroy(): void { if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId); for (const user of this.users) { this.leave(user); @@ -132,7 +133,7 @@ export class Group implements Movable { this.wasDestroyed = true; } - get getSize(){ + get getSize() { return this.users.size; } } diff --git a/back/src/Model/Movable.ts b/back/src/Model/Movable.ts index 173db0ae..ca586b7c 100644 --- a/back/src/Model/Movable.ts +++ b/back/src/Model/Movable.ts @@ -1,8 +1,8 @@ -import {PositionInterface} from "_Model/PositionInterface"; +import { PositionInterface } from "_Model/PositionInterface"; /** * A physical object that can be placed into a Zone */ export interface Movable { - getPosition(): PositionInterface + getPosition(): PositionInterface; } diff --git a/back/src/Model/PositionInterface.ts b/back/src/Model/PositionInterface.ts index d3b0dd47..65636759 100644 --- a/back/src/Model/PositionInterface.ts +++ b/back/src/Model/PositionInterface.ts @@ -1,4 +1,4 @@ export interface PositionInterface { - x: number, - y: number + x: number; + y: number; } diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index 275bf9d0..c34c1ef1 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -8,12 +8,12 @@ * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * number of players around the current player. */ -import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone"; -import {Movable} from "_Model/Movable"; -import {PositionInterface} from "_Model/PositionInterface"; -import {ZoneSocket} from "../RoomManager"; -import {User} from "_Model/User"; -import {EmoteEventMessage} from "../Messages/generated/messages_pb"; +import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone"; +import { Movable } from "_Model/Movable"; +import { PositionInterface } from "_Model/PositionInterface"; +import { ZoneSocket } from "../RoomManager"; +import { User } from "_Model/User"; +import { EmoteEventMessage } from "../Messages/generated/messages_pb"; interface ZoneDescriptor { i: number; @@ -21,19 +21,24 @@ interface ZoneDescriptor { } export class PositionNotifier { - // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) private zones: Zone[][] = []; - constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback, private onEmote: EmoteCallback) { - } + constructor( + private zoneWidth: number, + private zoneHeight: number, + private onUserEnters: EntersCallback, + private onUserMoves: MovesCallback, + private onUserLeaves: LeavesCallback, + private onEmote: EmoteCallback + ) {} private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), - } + }; } public enter(thing: Movable): void { @@ -100,6 +105,5 @@ export class PositionNotifier { const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); const zone = this.getZone(zoneDesc.i, zoneDesc.j); zone.emitEmoteEvent(emoteEventMessage); - } } diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts index 3ac62bca..d1de8800 100644 --- a/back/src/Model/RoomIdentifier.ts +++ b/back/src/Model/RoomIdentifier.ts @@ -1,30 +1,30 @@ //helper functions to parse room IDs export const isRoomAnonymous = (roomID: string): boolean => { - if (roomID.startsWith('_/')) { + if (roomID.startsWith("_/")) { return true; - } else if(roomID.startsWith('@/')) { + } else if (roomID.startsWith("@/")) { return false; } else { - throw new Error('Incorrect room ID: '+roomID); + throw new Error("Incorrect room ID: " + roomID); } -} +}; export const extractRoomSlugPublicRoomId = (roomId: string): string => { - const idParts = roomId.split('/'); - if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); - return idParts.slice(2).join('/'); -} + const idParts = roomId.split("/"); + if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId); + return idParts.slice(2).join("/"); +}; export interface extractDataFromPrivateRoomIdResponse { organizationSlug: string; worldSlug: string; roomSlug: string; } export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { - const idParts = roomId.split('/'); - if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const idParts = roomId.split("/"); + if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId); const organizationSlug = idParts[1]; const worldSlug = idParts[2]; const roomSlug = idParts[3]; - return {organizationSlug, worldSlug, roomSlug} -} \ No newline at end of file + return { organizationSlug, worldSlug, roomSlug }; +}; diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index 4a3e75ec..186fb32a 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -1,11 +1,17 @@ import { Group } from "./Group"; import { PointInterface } from "./Websocket/PointInterface"; -import {Zone} from "_Model/Zone"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {ServerDuplexStream} from "grpc"; -import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; +import { Zone } from "_Model/Zone"; +import { Movable } from "_Model/Movable"; +import { PositionNotifier } from "_Model/PositionNotifier"; +import { ServerDuplexStream } from "grpc"; +import { + BatchMessage, + CompanionMessage, + PusherToBackMessage, + ServerToClientMessage, + SubMessage, +} from "../Messages/generated/messages_pb"; +import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; export type UserSocket = ServerDuplexStream; @@ -22,7 +28,7 @@ export class User implements Movable { private positionNotifier: PositionNotifier, public readonly socket: UserSocket, public readonly tags: string[], - public readonly visitCardUrl: string|null, + public readonly visitCardUrl: string | null, public readonly name: string, public readonly characterLayers: CharacterLayer[], public readonly companion?: CompanionMessage @@ -42,9 +48,8 @@ export class User implements Movable { this.positionNotifier.updatePosition(this, position, oldPosition); } - private batchedMessages: BatchMessage = new BatchMessage(); - private batchTimeout: NodeJS.Timeout|null = null; + private batchTimeout: NodeJS.Timeout | null = null; public emitInBatch(payload: SubMessage): void { this.batchedMessages.addPayload(payload); diff --git a/back/src/Model/Websocket/CharacterLayer.ts b/back/src/Model/Websocket/CharacterLayer.ts index 13d838ee..3e428790 100644 --- a/back/src/Model/Websocket/CharacterLayer.ts +++ b/back/src/Model/Websocket/CharacterLayer.ts @@ -1,4 +1,4 @@ export interface CharacterLayer { - name: string, - url: string|undefined + name: string; + url: string | undefined; } diff --git a/back/src/Model/Websocket/ItemEventMessage.ts b/back/src/Model/Websocket/ItemEventMessage.ts index b1f9203e..1bb7f615 100644 --- a/back/src/Model/Websocket/ItemEventMessage.ts +++ b/back/src/Model/Websocket/ItemEventMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isItemEventMessageInterface = - new tg.IsInterface().withProperties({ +export const isItemEventMessageInterface = new tg.IsInterface() + .withProperties({ itemId: tg.isNumber, event: tg.isString, state: tg.isUnknown, parameters: tg.isUnknown, - }).get(); + }) + .get(); export type ItemEventMessageInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/MessageUserPosition.ts b/back/src/Model/Websocket/MessageUserPosition.ts index ee43d58c..19b57d2e 100644 --- a/back/src/Model/Websocket/MessageUserPosition.ts +++ b/back/src/Model/Websocket/MessageUserPosition.ts @@ -1,7 +1,10 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; -export class Point implements PointInterface{ - constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { - } +export class Point implements PointInterface { + constructor( + public x: number, + public y: number, + public direction: string = "none", + public moving: boolean = false + ) {} } - diff --git a/back/src/Model/Websocket/PointInterface.ts b/back/src/Model/Websocket/PointInterface.ts index afb07a23..d7c7826e 100644 --- a/back/src/Model/Websocket/PointInterface.ts +++ b/back/src/Model/Websocket/PointInterface.ts @@ -7,11 +7,12 @@ import * as tg from "generic-type-guard"; readonly moving: boolean; }*/ -export const isPointInterface = - new tg.IsInterface().withProperties({ +export const isPointInterface = new tg.IsInterface() + .withProperties({ x: tg.isNumber, y: tg.isNumber, direction: tg.isString, - moving: tg.isBoolean - }).get(); + moving: tg.isBoolean, + }) + .get(); export type PointInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/ProtobufUtils.ts b/back/src/Model/Websocket/ProtobufUtils.ts index b85a4257..68817a4f 100644 --- a/back/src/Model/Websocket/ProtobufUtils.ts +++ b/back/src/Model/Websocket/ProtobufUtils.ts @@ -1,34 +1,33 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; import { CharacterLayerMessage, ItemEventMessage, PointMessage, - PositionMessage + PositionMessage, } from "../../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; +import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; import Direction = PositionMessage.Direction; -import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; -import {PositionInterface} from "_Model/PositionInterface"; +import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; +import { PositionInterface } from "_Model/PositionInterface"; export class ProtobufUtils { - public static toPositionMessage(point: PointInterface): PositionMessage { let direction: Direction; switch (point.direction) { - case 'up': + case "up": direction = Direction.UP; break; - case 'down': + case "down": direction = Direction.DOWN; break; - case 'left': + case "left": direction = Direction.LEFT; break; - case 'right': + case "right": direction = Direction.RIGHT; break; default: - throw new Error('unexpected direction'); + throw new Error("unexpected direction"); } const position = new PositionMessage(); @@ -44,16 +43,16 @@ export class ProtobufUtils { let direction: string; switch (position.getDirection()) { case Direction.UP: - direction = 'up'; + direction = "up"; break; case Direction.DOWN: - direction = 'down'; + direction = "down"; break; case Direction.LEFT: - direction = 'left'; + direction = "left"; break; case Direction.RIGHT: - direction = 'right'; + direction = "right"; break; default: throw new Error("Unexpected direction"); @@ -82,7 +81,7 @@ export class ProtobufUtils { event: itemEventMessage.getEvent(), parameters: JSON.parse(itemEventMessage.getParametersjson()), state: JSON.parse(itemEventMessage.getStatejson()), - } + }; } public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { @@ -96,7 +95,7 @@ export class ProtobufUtils { } public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { - return characterLayers.map(function(characterLayer): CharacterLayerMessage { + return characterLayers.map(function (characterLayer): CharacterLayerMessage { const message = new CharacterLayerMessage(); message.setName(characterLayer.name); if (characterLayer.url) { @@ -107,7 +106,7 @@ export class ProtobufUtils { } public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] { - return characterLayers.map(function(characterLayer): CharacterLayer { + return characterLayers.map(function (characterLayer): CharacterLayer { const url = characterLayer.getUrl(); return { name: characterLayer.getName(), diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index ffb172bb..d236e489 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -1,35 +1,52 @@ -import {User} from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {Movable} from "./Movable"; -import {Group} from "./Group"; -import {ZoneSocket} from "../RoomManager"; -import {EmoteEventMessage} from "../Messages/generated/messages_pb"; +import { User } from "./User"; +import { PositionInterface } from "_Model/PositionInterface"; +import { Movable } from "./Movable"; +import { Group } from "./Group"; +import { ZoneSocket } from "../RoomManager"; +import { EmoteEventMessage } from "../Messages/generated/messages_pb"; -export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void; +export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void; export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; -export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void; +export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void; export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; export class Zone { private things: Set = new Set(); private listeners: Set = new Set(); - - - constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { } + + constructor( + private onEnters: EntersCallback, + private onMoves: MovesCallback, + private onLeaves: LeavesCallback, + private onEmote: EmoteCallback, + public readonly x: number, + public readonly y: number + ) {} /** * A user/thing leaves the zone */ - public leave(thing: Movable, newZone: Zone|null) { + public leave(thing: Movable, newZone: Zone | null) { const result = this.things.delete(thing); if (!result) { if (thing instanceof User) { - throw new Error('Could not find user in zone '+thing.id); + throw new Error("Could not find user in zone " + thing.id); } if (thing instanceof Group) { - throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')'); + throw new Error( + "Could not find group " + + thing.getId() + + " in zone (" + + this.x + + "," + + this.y + + "). Position of group: (" + + thing.getPosition().x + + "," + + thing.getPosition().y + + ")" + ); } - } this.notifyLeft(thing, newZone); } @@ -37,13 +54,13 @@ export class Zone { /** * Notify listeners of this zone that this user/thing left */ - private notifyLeft(thing: Movable, newZone: Zone|null) { + private notifyLeft(thing: Movable, newZone: Zone | null) { for (const listener of this.listeners) { this.onLeaves(thing, newZone, listener); } } - public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { + public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { this.things.add(thing); this.notifyEnter(thing, oldZone, position); } @@ -51,13 +68,12 @@ export class Zone { /** * Notify listeners of this zone that this user entered */ - private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { + private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { for (const listener of this.listeners) { this.onEnters(thing, oldZone, listener); } } - public move(thing: Movable, position: PositionInterface) { if (!this.things.has(thing)) { this.things.add(thing); @@ -67,7 +83,7 @@ export class Zone { for (const listener of this.listeners) { //if (listener !== thing) { - this.onMoves(thing,position, listener); + this.onMoves(thing, position, listener); //} } } @@ -89,6 +105,5 @@ export class Zone { for (const listener of this.listeners) { this.onEmote(emoteEventMessage, listener); } - } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 19266687..9aaf1edb 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -1,4 +1,4 @@ -import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb"; +import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb"; import { AdminGlobalMessage, AdminMessage, @@ -11,92 +11,114 @@ import { JoinRoomMessage, PlayGlobalMessage, PusherToBackMessage, - QueryJitsiJwtMessage, RefreshRoomPromptMessage, + QueryJitsiJwtMessage, + RefreshRoomPromptMessage, ServerToAdminClientMessage, ServerToClientMessage, SilentMessage, UserMovesMessage, - WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, - ZoneMessage + WebRtcSignalToServerMessage, + WorldFullWarningToRoomMessage, + ZoneMessage, } from "./Messages/generated/messages_pb"; -import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc"; -import {socketManager} from "./Services/SocketManager"; -import {emitError} from "./Services/MessageHelpers"; -import {User, UserSocket} from "./Model/User"; -import {GameRoom} from "./Model/GameRoom"; +import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc"; +import { socketManager } from "./Services/SocketManager"; +import { emitError } from "./Services/MessageHelpers"; +import { User, UserSocket } from "./Model/User"; +import { GameRoom } from "./Model/GameRoom"; import Debug from "debug"; -import {Admin} from "./Model/Admin"; +import { Admin } from "./Model/Admin"; -const debug = Debug('roommanager'); +const debug = Debug("roommanager"); export type AdminSocket = ServerDuplexStream; export type ZoneSocket = ServerWritableStream; const roomManager: IRoomManagerServer = { joinRoom: (call: UserSocket): void => { - console.log('joinRoom called'); + console.log("joinRoom called"); - let room: GameRoom|null = null; - let user: User|null = null; + let room: GameRoom | null = null; + let user: User | null = null; - call.on('data', (message: PusherToBackMessage) => { + call.on("data", (message: PusherToBackMessage) => { try { if (room === null || user === null) { if (message.hasJoinroommessage()) { - socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => { - if (call.writable) { - room = gameRoom; - user = myUser; - } else { - //Connexion may have been closed before the init was finished, so we have to manually disconnect the user. - socketManager.leaveRoom(gameRoom, myUser); - } - }); + socketManager + .handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage) + .then(({ room: gameRoom, user: myUser }) => { + if (call.writable) { + room = gameRoom; + user = myUser; + } else { + //Connexion may have been closed before the init was finished, so we have to manually disconnect the user. + socketManager.leaveRoom(gameRoom, myUser); + } + }); } else { - throw new Error('The first message sent MUST be of type JoinRoomMessage'); + throw new Error("The first message sent MUST be of type JoinRoomMessage"); } } else { if (message.hasJoinroommessage()) { - throw new Error('Cannot call JoinRoomMessage twice!'); + throw new Error("Cannot call JoinRoomMessage twice!"); } else if (message.hasUsermovesmessage()) { - socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage); + socketManager.handleUserMovesMessage( + room, + user, + message.getUsermovesmessage() as UserMovesMessage + ); } else if (message.hasSilentmessage()) { socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitVideo( + room, + user, + message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitScreenSharing( + room, + user, + message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasPlayglobalmessage()) { socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); - } else if (message.hasQueryjitsijwtmessage()){ - socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); - } else if (message.hasEmotepromptmessage()){ - socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage); - }else if (message.hasSendusermessage()) { + } else if (message.hasQueryjitsijwtmessage()) { + socketManager.handleQueryJitsiJwtMessage( + user, + message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage + ); + } else if (message.hasEmotepromptmessage()) { + socketManager.handleEmoteEventMessage( + room, + user, + message.getEmotepromptmessage() as EmotePromptMessage + ); + } else if (message.hasSendusermessage()) { const sendUserMessage = message.getSendusermessage(); - if(sendUserMessage !== undefined) { + if (sendUserMessage !== undefined) { socketManager.handlerSendUserMessage(user, sendUserMessage); } - }else if (message.hasBanusermessage()) { + } else if (message.hasBanusermessage()) { const banUserMessage = message.getBanusermessage(); - if(banUserMessage !== undefined) { + if (banUserMessage !== undefined) { socketManager.handlerBanUserMessage(room, user, banUserMessage); } } else { - throw new Error('Unhandled message type'); + throw new Error("Unhandled message type"); } } } catch (e) { emitError(call, e); call.end(); } - }); - call.on('end', () => { - debug('joinRoom ended'); + call.on("end", () => { + debug("joinRoom ended"); if (user !== null && room !== null) { socketManager.leaveRoom(room, user); } @@ -105,41 +127,40 @@ const roomManager: IRoomManagerServer = { user = null; }); - call.on('error', (err: Error) => { - console.error('An error occurred in joinRoom stream:', err); + call.on("error", (err: Error) => { + console.error("An error occurred in joinRoom stream:", err); }); - }, listenZone(call: ZoneSocket): void { - debug('listenZone called'); + debug("listenZone called"); const zoneMessage = call.request; socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); - call.on('cancelled', () => { - debug('listenZone cancelled'); + call.on("cancelled", () => { + debug("listenZone cancelled"); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); - }) - - call.on('close', () => { - debug('listenZone connection closed'); + }); + + call.on("close", () => { + debug("listenZone connection closed"); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); - }).on('error', (e) => { - console.error('An error occurred in listenZone stream:', e); + }).on("error", (e) => { + console.error("An error occurred in listenZone stream:", e); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); }); }, adminRoom(call: AdminSocket): void { - console.log('adminRoom called'); + console.log("adminRoom called"); const admin = new Admin(call); - let room: GameRoom|null = null; + let room: GameRoom | null = null; - call.on('data', (message: AdminPusherToBackMessage) => { + call.on("data", (message: AdminPusherToBackMessage) => { try { if (room === null) { if (message.hasSubscribetoroom()) { @@ -148,18 +169,17 @@ const roomManager: IRoomManagerServer = { room = gameRoom; }); } else { - throw new Error('The first message sent MUST be of type JoinRoomMessage'); + throw new Error("The first message sent MUST be of type JoinRoomMessage"); } } } catch (e) { emitError(call, e); call.end(); } - }); - call.on('end', () => { - debug('joinRoom ended'); + call.on("end", () => { + debug("joinRoom ended"); if (room !== null) { socketManager.leaveAdminRoom(room, admin); } @@ -167,18 +187,21 @@ const roomManager: IRoomManagerServer = { room = null; }); - call.on('error', (err: Error) => { - console.error('An error occurred in joinAdminRoom stream:', err); + call.on("error", (err: Error) => { + console.error("An error occurred in joinAdminRoom stream:", err); }); }, sendAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - - socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage()); + socketManager.sendAdminMessage( + call.request.getRoomid(), + call.request.getRecipientuuid(), + call.request.getMessage() + ); callback(null, new EmptyMessage()); }, sendGlobalAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - throw new Error('Not implemented yet'); + throw new Error("Not implemented yet"); // TODO callback(null, new EmptyMessage()); }, @@ -192,14 +215,20 @@ const roomManager: IRoomManagerServer = { socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()); callback(null, new EmptyMessage()); }, - sendWorldFullWarningToRoom(call: ServerUnaryCall, callback: sendUnaryData): void { + sendWorldFullWarningToRoom( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { socketManager.dispatchWorlFullWarning(call.request.getRoomid()); callback(null, new EmptyMessage()); }, - sendRefreshRoomPrompt(call: ServerUnaryCall, callback: sendUnaryData): void { + sendRefreshRoomPrompt( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { socketManager.dispatchRoomRefresh(call.request.getRoomid()); callback(null, new EmptyMessage()); }, }; -export {roomManager}; +export { roomManager }; diff --git a/back/src/Server/server/app.ts b/back/src/Server/server/app.ts index 3b98a9b3..4c422d5c 100644 --- a/back/src/Server/server/app.ts +++ b/back/src/Server/server/app.ts @@ -1,13 +1,13 @@ -import { App as _App, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { App as _App, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class App extends (_App) { - constructor(options: AppOptions = {}) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions = {}) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default App; diff --git a/back/src/Server/server/baseapp.ts b/back/src/Server/server/baseapp.ts index accd8a99..6d973ac7 100644 --- a/back/src/Server/server/baseapp.ts +++ b/back/src/Server/server/baseapp.ts @@ -1,116 +1,109 @@ -import { Readable } from 'stream'; -import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { Readable } from "stream"; +import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; -import formData from './formdata'; -import { stob } from './utils'; -import { Handler } from './types'; -import {join} from "path"; +import formData from "./formdata"; +import { stob } from "./utils"; +import { Handler } from "./types"; +import { join } from "path"; -const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; +const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"]; const noOp = () => true; const handleBody = (res: HttpResponse, req: HttpRequest) => { - const contType = req.getHeader('content-type'); + const contType = req.getHeader("content-type"); - res.bodyStream = function() { - const stream = new Readable(); - stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method + res.bodyStream = function () { + const stream = new Readable(); + stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method - this.onData((ab: ArrayBuffer, isLast: boolean) => { - // uint and then slicing is bit faster than slice and then uint - stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any - if (isLast) { - stream.push(null); - } - }); + this.onData((ab: ArrayBuffer, isLast: boolean) => { + // uint and then slicing is bit faster than slice and then uint + stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any + if (isLast) { + stream.push(null); + } + }); - return stream; - }; + return stream; + }; - res.body = () => stob(res.bodyStream()); + res.body = () => stob(res.bodyStream()); - if (contType.includes('application/json')) - res.json = async () => JSON.parse(await res.body()); - if (contTypes.map(t => contType.includes(t)).includes(true)) - res.formData = formData.bind(res, contType); + if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body()); + if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType); }; class BaseApp { - _sockets = new Map(); - ws!: TemplatedApp['ws']; - get!: TemplatedApp['get']; - _post!: TemplatedApp['post']; - _put!: TemplatedApp['put']; - _patch!: TemplatedApp['patch']; - _listen!: TemplatedApp['listen']; + _sockets = new Map(); + ws!: TemplatedApp["ws"]; + get!: TemplatedApp["get"]; + _post!: TemplatedApp["post"]; + _put!: TemplatedApp["put"]; + _patch!: TemplatedApp["patch"]; + _listen!: TemplatedApp["listen"]; - post(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._post(pattern, (res, req) => { - handleBody(res, req); - handler(res, req); - }); - return this; - } + post(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._post(pattern, (res, req) => { + handleBody(res, req); + handler(res, req); + }); + return this; + } - put(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._put(pattern, (res, req) => { - handleBody(res, req); + put(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._put(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - patch(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._patch(pattern, (res, req) => { - handleBody(res, req); + patch(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._patch(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - listen(h: string | number, p: Function | number = noOp, cb?: Function) { - if (typeof p === 'number' && typeof h === 'string') { - this._listen(h, p, socket => { - this._sockets.set(p, socket); - if (cb === undefined) { - throw new Error('cb undefined'); + listen(h: string | number, p: Function | number = noOp, cb?: Function) { + if (typeof p === "number" && typeof h === "string") { + this._listen(h, p, (socket) => { + this._sockets.set(p, socket); + if (cb === undefined) { + throw new Error("cb undefined"); + } + cb(socket); + }); + } else if (typeof h === "number" && typeof p === "function") { + this._listen(h, (socket) => { + this._sockets.set(h, socket); + p(socket); + }); + } else { + throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)"); } - cb(socket); - }); - } else if (typeof h === 'number' && typeof p === 'function') { - this._listen(h, socket => { - this._sockets.set(h, socket); - p(socket); - }); - } else { - throw Error( - 'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)' - ); + + return this; } - return this; - } - - close(port: null | number = null) { - if (port) { - this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); - this._sockets.delete(port); - } else { - this._sockets.forEach(app => { - us_listen_socket_close(app); - }); - this._sockets.clear(); + close(port: null | number = null) { + if (port) { + this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); + this._sockets.delete(port); + } else { + this._sockets.forEach((app) => { + us_listen_socket_close(app); + }); + this._sockets.clear(); + } + return this; } - return this; - } } export default BaseApp; diff --git a/back/src/Server/server/formdata.ts b/back/src/Server/server/formdata.ts index 9dd08440..66e51db4 100644 --- a/back/src/Server/server/formdata.ts +++ b/back/src/Server/server/formdata.ts @@ -1,100 +1,99 @@ -import { createWriteStream } from 'fs'; -import { join, dirname } from 'path'; -import Busboy from 'busboy'; -import mkdirp from 'mkdirp'; +import { createWriteStream } from "fs"; +import { join, dirname } from "path"; +import Busboy from "busboy"; +import mkdirp from "mkdirp"; function formData( - contType: string, - options: busboy.BusboyConfig & { - abortOnLimit?: boolean; - tmpDir?: string; - onFile?: ( - fieldname: string, - file: NodeJS.ReadableStream, - filename: string, - encoding: string, - mimetype: string - ) => string; - onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - filename?: (oldName: string) => string; - } = {} + contType: string, + options: busboy.BusboyConfig & { + abortOnLimit?: boolean; + tmpDir?: string; + onFile?: ( + fieldname: string, + file: NodeJS.ReadableStream, + filename: string, + encoding: string, + mimetype: string + ) => string; + onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + filename?: (oldName: string) => string; + } = {} ) { - console.log('Enter form data'); - options.headers = { - 'content-type': contType - }; + console.log("Enter form data"); + options.headers = { + "content-type": contType, + }; - return new Promise((resolve, reject) => { - const busb = new Busboy(options); - const ret = {}; + return new Promise((resolve, reject) => { + const busb = new Busboy(options); + const ret = {}; - this.bodyStream().pipe(busb); + this.bodyStream().pipe(busb); - busb.on('limit', () => { - if (options.abortOnLimit) { - reject(Error('limit')); - } + busb.on("limit", () => { + if (options.abortOnLimit) { + reject(Error("limit")); + } + }); + + busb.on("file", function (fieldname, file, filename, encoding, mimetype) { + const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = { + filename, + encoding, + mimetype, + filePath: undefined, + }; + + if (typeof options.tmpDir === "string") { + if (typeof options.filename === "function") filename = options.filename(filename); + const fileToSave = join(options.tmpDir, filename); + mkdirp(dirname(fileToSave)); + + file.pipe(createWriteStream(fileToSave)); + value.filePath = fileToSave; + } + if (typeof options.onFile === "function") { + value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; + } + + setRetValue(ret, fieldname, value); + }); + + busb.on("field", function (fieldname, value) { + if (typeof options.onField === "function") options.onField(fieldname, value); + + setRetValue(ret, fieldname, value); + }); + + busb.on("finish", function () { + resolve(ret); + }); + + busb.on("error", reject); }); - - busb.on('file', function(fieldname, file, filename, encoding, mimetype) { - const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = { - filename, - encoding, - mimetype, - filePath: undefined - }; - - if (typeof options.tmpDir === 'string') { - if (typeof options.filename === 'function') filename = options.filename(filename); - const fileToSave = join(options.tmpDir, filename); - mkdirp(dirname(fileToSave)); - - file.pipe(createWriteStream(fileToSave)); - value.filePath = fileToSave; - } - if (typeof options.onFile === 'function') { - value.filePath = - options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; - } - - setRetValue(ret, fieldname, value); - }); - - busb.on('field', function(fieldname, value) { - if (typeof options.onField === 'function') options.onField(fieldname, value); - - setRetValue(ret, fieldname, value); - }); - - busb.on('finish', function() { - resolve(ret); - }); - - busb.on('error', reject); - }); } function setRetValue( - ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any - fieldname: string, - value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any + ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + fieldname: string, + value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any ) { - if (fieldname.endsWith('[]')) { - fieldname = fieldname.slice(0, fieldname.length - 2); - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); + if (fieldname.endsWith("[]")) { + fieldname = fieldname.slice(0, fieldname.length - 2); + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else { + ret[fieldname] = [value]; + } } else { - ret[fieldname] = [value]; + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else if (ret[fieldname]) { + ret[fieldname] = [ret[fieldname], value]; + } else { + ret[fieldname] = value; + } } - } else { - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); - } else if (ret[fieldname]) { - ret[fieldname] = [ret[fieldname], value]; - } else { - ret[fieldname] = value; - } - } } export default formData; diff --git a/back/src/Server/server/sslapp.ts b/back/src/Server/server/sslapp.ts index 46ae89a5..80df0e4a 100644 --- a/back/src/Server/server/sslapp.ts +++ b/back/src/Server/server/sslapp.ts @@ -1,13 +1,13 @@ -import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class SSLApp extends (_SSLApp) { - constructor(options: AppOptions) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default SSLApp; diff --git a/back/src/Server/server/types.ts b/back/src/Server/server/types.ts index 3d0f48c7..afc21d17 100644 --- a/back/src/Server/server/types.ts +++ b/back/src/Server/server/types.ts @@ -1,9 +1,9 @@ -import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; export type UwsApp = { - (options: AppOptions): TemplatedApp; - new (options: AppOptions): TemplatedApp; - prototype: TemplatedApp; + (options: AppOptions): TemplatedApp; + new (options: AppOptions): TemplatedApp; + prototype: TemplatedApp; }; export type Handler = (res: HttpResponse, req: HttpRequest) => void; diff --git a/back/src/Server/server/utils.ts b/back/src/Server/server/utils.ts index 80ea3938..9816de54 100644 --- a/back/src/Server/server/utils.ts +++ b/back/src/Server/server/utils.ts @@ -1,37 +1,36 @@ -import { ReadStream } from 'fs'; +import { ReadStream } from "fs"; -function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any - const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( - Object.keys(from) - ); - ownProps.forEach(prop => { - if (prop === 'constructor' || from[prop] === undefined) return; - if (who[prop] && overwrite) { - who[`_${prop}`] = who[prop]; - } - if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); - else who[prop] = from[prop]; - }); +function extend(who: any, from: any, overwrite = true) { + // eslint-disable-line @typescript-eslint/no-explicit-any + const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); + ownProps.forEach((prop) => { + if (prop === "constructor" || from[prop] === undefined) return; + if (who[prop] && overwrite) { + who[`_${prop}`] = who[prop]; + } + if (typeof from[prop] === "function") who[prop] = from[prop].bind(who); + else who[prop] = from[prop]; + }); } function stob(stream: ReadStream): Promise { - return new Promise(resolve => { - const buffers: Buffer[] = []; - stream.on('data', buffers.push.bind(buffers)); + return new Promise((resolve) => { + const buffers: Buffer[] = []; + stream.on("data", buffers.push.bind(buffers)); - stream.on('end', () => { - switch (buffers.length) { - case 0: - resolve(Buffer.allocUnsafe(0)); - break; - case 1: - resolve(buffers[0]); - break; - default: - resolve(Buffer.concat(buffers)); - } + stream.on("end", () => { + switch (buffers.length) { + case 0: + resolve(Buffer.allocUnsafe(0)); + break; + case 1: + resolve(buffers[0]); + break; + default: + resolve(Buffer.concat(buffers)); + } + }); }); - }); } export { extend, stob }; diff --git a/back/src/Server/sifrr.server.ts b/back/src/Server/sifrr.server.ts index 47fba02c..4ef03721 100644 --- a/back/src/Server/sifrr.server.ts +++ b/back/src/Server/sifrr.server.ts @@ -1,19 +1,19 @@ -import { parse } from 'query-string'; -import { HttpRequest } from 'uWebSockets.js'; -import App from './server/app'; -import SSLApp from './server/sslapp'; -import * as types from './server/types'; +import { parse } from "query-string"; +import { HttpRequest } from "uWebSockets.js"; +import App from "./server/app"; +import SSLApp from "./server/sslapp"; +import * as types from "./server/types"; const getQuery = (req: HttpRequest) => { - return parse(req.getQuery()); + return parse(req.getQuery()); }; export { App, SSLApp, getQuery }; -export * from './server/types'; +export * from "./server/types"; export default { - App, - SSLApp, - getQuery, - ...types + App, + SSLApp, + getQuery, + ...types, }; diff --git a/back/src/Services/ArrayHelper.ts b/back/src/Services/ArrayHelper.ts index 67321d1b..8af1da9f 100644 --- a/back/src/Services/ArrayHelper.ts +++ b/back/src/Services/ArrayHelper.ts @@ -1,3 +1,3 @@ -export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { - return array1.filter(value => array2.includes(value)).length > 0; -} \ No newline at end of file +export const arrayIntersect = (array1: string[], array2: string[]): boolean => { + return array1.filter((value) => array2.includes(value)).length > 0; +}; diff --git a/back/src/Services/ClientEventsEmitter.ts b/back/src/Services/ClientEventsEmitter.ts index 381137a1..0f56d55c 100644 --- a/back/src/Services/ClientEventsEmitter.ts +++ b/back/src/Services/ClientEventsEmitter.ts @@ -1,7 +1,7 @@ -const EventEmitter = require('events'); +const EventEmitter = require("events"); -const clientJoinEvent = 'clientJoin'; -const clientLeaveEvent = 'clientLeave'; +const clientJoinEvent = "clientJoin"; +const clientLeaveEvent = "clientLeave"; class ClientEventsEmitter extends EventEmitter { emitClientJoin(clientUUid: string, roomId: string): void { diff --git a/back/src/Services/CpuTracker.ts b/back/src/Services/CpuTracker.ts index c7d57f3d..3d06ca70 100644 --- a/back/src/Services/CpuTracker.ts +++ b/back/src/Services/CpuTracker.ts @@ -1,6 +1,6 @@ -import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; +import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable"; -function secNSec2ms(secNSec: Array|number) { +function secNSec2ms(secNSec: Array | number) { if (Array.isArray(secNSec)) { return secNSec[0] * 1000 + secNSec[1] / 1000000; } @@ -12,17 +12,17 @@ class CpuTracker { private overHeating: boolean = false; constructor() { - let time = process.hrtime.bigint() - let usage = process.cpuUsage() + let time = process.hrtime.bigint(); + let usage = process.cpuUsage(); setInterval(() => { const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage) - usage = process.cpuUsage() + const elapUsage = process.cpuUsage(usage); + usage = process.cpuUsage(); const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user) - const elapSystMS = secNSec2ms(elapUsage.system) - this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) + const elapUserMS = secNSec2ms(elapUsage.user); + const elapSystMS = secNSec2ms(elapUsage.system); + this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000); time = elapTime; diff --git a/back/src/Services/GaugeManager.ts b/back/src/Services/GaugeManager.ts index 80712856..6d2183d8 100644 --- a/back/src/Services/GaugeManager.ts +++ b/back/src/Services/GaugeManager.ts @@ -1,4 +1,4 @@ -import {Counter, Gauge} from "prom-client"; +import { Counter, Gauge } from "prom-client"; //this class should manage all the custom metrics used by prometheus class GaugeManager { @@ -10,29 +10,29 @@ class GaugeManager { constructor() { this.nbRoomsGauge = new Gauge({ - name: 'workadventure_nb_rooms', - help: 'Number of active rooms' + name: "workadventure_nb_rooms", + help: "Number of active rooms", }); this.nbClientsGauge = new Gauge({ - name: 'workadventure_nb_sockets', - help: 'Number of connected sockets', - labelNames: [ ] + name: "workadventure_nb_sockets", + help: "Number of connected sockets", + labelNames: [], }); this.nbClientsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_clients_per_room', - help: 'Number of clients per room', - labelNames: [ 'room' ] + name: "workadventure_nb_clients_per_room", + help: "Number of clients per room", + labelNames: ["room"], }); this.nbGroupsPerRoomCounter = new Counter({ - name: 'workadventure_counter_groups_per_room', - help: 'Counter of groups per room', - labelNames: [ 'room' ] + name: "workadventure_counter_groups_per_room", + help: "Counter of groups per room", + labelNames: ["room"], }); this.nbGroupsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_groups_per_room', - help: 'Number of groups per room', - labelNames: [ 'room' ] + name: "workadventure_nb_groups_per_room", + help: "Number of groups per room", + labelNames: ["room"], }); } @@ -54,13 +54,13 @@ class GaugeManager { } incNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomCounter.inc({ room: roomId }) - this.nbGroupsPerRoomGauge.inc({ room: roomId }) + this.nbGroupsPerRoomCounter.inc({ room: roomId }); + this.nbGroupsPerRoomGauge.inc({ room: roomId }); } - + decNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomGauge.dec({ room: roomId }) + this.nbGroupsPerRoomGauge.dec({ room: roomId }); } } -export const gaugeManager = new GaugeManager(); \ No newline at end of file +export const gaugeManager = new GaugeManager(); diff --git a/back/src/Services/MessageHelpers.ts b/back/src/Services/MessageHelpers.ts index b2600a4a..493f7173 100644 --- a/back/src/Services/MessageHelpers.ts +++ b/back/src/Services/MessageHelpers.ts @@ -1,5 +1,5 @@ -import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb"; -import {UserSocket} from "_Model/User"; +import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb"; +import { UserSocket } from "_Model/User"; export function emitError(Client: UserSocket, message: string): void { const errorMessage = new ErrorMessage(); @@ -9,7 +9,7 @@ export function emitError(Client: UserSocket, message: string): void { serverToClientMessage.setErrormessage(errorMessage); //if (!Client.disconnecting) { - Client.write(serverToClientMessage); + Client.write(serverToClientMessage); //} console.warn(message); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index a56a1ac4..e61763cd 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -1,4 +1,4 @@ -import {GameRoom} from "../Model/GameRoom"; +import { GameRoom } from "../Model/GameRoom"; import { ItemEventMessage, ItemStateMessage, @@ -27,39 +27,39 @@ import { WorldFullWarningMessage, UserLeftZoneMessage, EmoteEventMessage, - BanUserMessage, RefreshRoomMessage, EmotePromptMessage, + BanUserMessage, + RefreshRoomMessage, + EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {User, UserSocket} from "../Model/User"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {Group} from "../Model/Group"; -import {cpuTracker} from "./CpuTracker"; +import { User, UserSocket } from "../Model/User"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { Group } from "../Model/Group"; +import { cpuTracker } from "./CpuTracker"; import { GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY, - TURN_STATIC_AUTH_SECRET + TURN_STATIC_AUTH_SECRET, } from "../Enum/EnvironmentVariable"; -import {Movable} from "../Model/Movable"; -import {PositionInterface} from "../Model/PositionInterface"; +import { Movable } from "../Model/Movable"; +import { PositionInterface } from "../Model/PositionInterface"; import Jwt from "jsonwebtoken"; -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {clientEventsEmitter} from "./ClientEventsEmitter"; -import {gaugeManager} from "./GaugeManager"; -import {ZoneSocket} from "../RoomManager"; -import {Zone} from "_Model/Zone"; +import { JITSI_URL } from "../Enum/EnvironmentVariable"; +import { clientEventsEmitter } from "./ClientEventsEmitter"; +import { gaugeManager } from "./GaugeManager"; +import { ZoneSocket } from "../RoomManager"; +import { Zone } from "_Model/Zone"; import Debug from "debug"; -import {Admin} from "_Model/Admin"; +import { Admin } from "_Model/Admin"; import crypto from "crypto"; - -const debug = Debug('sockermanager'); +const debug = Debug("sockermanager"); function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void { // TODO: should we batch those every 100ms? const batchMessage = new BatchToPusherMessage(); batchMessage.addPayload(subMessage); - socket.write(batchMessage); } @@ -68,7 +68,6 @@ export class SocketManager { private rooms: Map = new Map(); constructor() { - clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { gaugeManager.incNbClientPerRoomGauge(roomId); }); @@ -77,16 +76,18 @@ export class SocketManager { }); } - public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { - + public async handleJoinRoom( + socket: UserSocket, + joinRoomMessage: JoinRoomMessage + ): Promise<{ room: GameRoom; user: User }> { //join new previous room - const {room, user} = await this.joinRoom(socket, joinRoomMessage); - + const { room, user } = await this.joinRoom(socket, joinRoomMessage); + if (!socket.writable) { - console.warn('Socket was aborted'); + console.warn("Socket was aborted"); return { room, - user + user, }; } const roomJoinedMessage = new RoomJoinedMessage(); @@ -108,9 +109,8 @@ export class SocketManager { return { room, - user + user, }; - } handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) { @@ -124,13 +124,12 @@ export class SocketManager { } if (position === undefined) { - throw new Error('Position not found in message'); + throw new Error("Position not found in message"); } const viewport = userMoves.viewport; if (viewport === undefined) { - throw new Error('Viewport not found in message'); + throw new Error("Viewport not found in message"); } - // update position in the world room.updatePosition(user, ProtobufUtils.toPointInterface(position)); @@ -189,7 +188,11 @@ export class SocketManager { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); if (remoteUser === undefined) { - console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); + console.warn( + "While exchanging a WebRTC signal: client with id ", + data.getReceiverid(), + " does not exist. This might be a race condition." + ); return; } @@ -197,8 +200,8 @@ export class SocketManager { webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcpassword(password); } @@ -207,7 +210,7 @@ export class SocketManager { serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); + remoteUser.socket.write(serverToClientMessage); //} } @@ -215,7 +218,11 @@ export class SocketManager { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); if (remoteUser === undefined) { - console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); + console.warn( + "While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", + data.getReceiverid(), + " does not exist. This might be a race condition." + ); return; } @@ -223,8 +230,8 @@ export class SocketManager { webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcpassword(password); } @@ -233,11 +240,11 @@ export class SocketManager { serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); + remoteUser.socket.write(serverToClientMessage); //} } - leaveRoom(room: GameRoom, user: User){ + leaveRoom(room: GameRoom, user: User) { // leave previous room and world try { //user leave previous world @@ -249,33 +256,39 @@ export class SocketManager { } } finally { clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); - console.log('A user left'); + console.log("A user left"); } } async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room - let world = this.rooms.get(roomId) - if(world === undefined){ + let world = this.rooms.get(roomId); + if (world === undefined) { world = new GameRoom( roomId, (user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.disConnectedUser(user, group), MINIMUM_DISTANCE, GROUP_RADIUS, - (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener), - (thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), - (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener), - (emoteEventMessage:EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener), + (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => + this.onZoneEnter(thing, fromZone, listener), + (thing: Movable, position: PositionInterface, listener: ZoneSocket) => + this.onClientMove(thing, position, listener), + (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => + this.onClientLeave(thing, newZone, listener), + (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => + this.onEmote(emoteEventMessage, listener) ); gaugeManager.incNbRoomGauge(); this.rooms.set(roomId, world); } - return Promise.resolve(world) + return Promise.resolve(world); } - private async joinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { - + private async joinRoom( + socket: UserSocket, + joinRoomMessage: JoinRoomMessage + ): Promise<{ room: GameRoom; user: User }> { const roomId = joinRoomMessage.getRoomid(); const room = await socketManager.getOrCreateRoom(roomId); @@ -284,15 +297,15 @@ export class SocketManager { const user = room.join(socket, joinRoomMessage); clientEventsEmitter.emitClientJoin(user.uuid, roomId); - console.log(new Date().toISOString() + ' A user joined'); - return {room, user}; + console.log(new Date().toISOString() + " A user joined"); + return { room, user }; } - private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) { + private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) { if (thing instanceof User) { const userJoinedZoneMessage = new UserJoinedZoneMessage(); if (!Number.isInteger(thing.id)) { - throw new Error('clientUser.userId is not an integer '+thing.id); + throw new Error("clientUser.userId is not an integer " + thing.id); } userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setName(thing.name); @@ -312,11 +325,11 @@ export class SocketManager { } else if (thing instanceof Group) { this.emitCreateUpdateGroupEvent(listener, fromZone, thing); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onClientMove(thing: Movable, position:PositionInterface, listener: ZoneSocket): void { + private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void { if (thing instanceof User) { const userMovedMessage = new UserMovedMessage(); userMovedMessage.setUserid(thing.id); @@ -331,21 +344,20 @@ export class SocketManager { } else if (thing instanceof Group) { this.emitCreateUpdateGroupEvent(listener, null, thing); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onClientLeave(thing: Movable, newZone: Zone|null, listener: ZoneSocket) { + private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) { if (thing instanceof User) { this.emitUserLeftEvent(listener, thing.id, newZone); } else if (thing instanceof Group) { this.emitDeleteGroupEvent(listener, thing.getId(), newZone); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) { const subMessage = new SubToPusherMessage(); subMessage.setEmoteeventmessage(emoteEventMessage); @@ -353,7 +365,7 @@ export class SocketManager { emitZoneMessage(subMessage, client); } - private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void { + private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void { const position = group.getPosition(); const pointMessage = new PointMessage(); pointMessage.setX(Math.floor(position.x)); @@ -371,7 +383,7 @@ export class SocketManager { //client.emitInBatch(subMessage); } - private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone|null): void { + private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void { const groupDeleteMessage = new GroupLeftZoneMessage(); groupDeleteMessage.setGroupid(groupId); groupDeleteMessage.setTozone(this.toProtoZone(newZone)); @@ -383,7 +395,7 @@ export class SocketManager { //user.emitInBatch(subMessage); } - private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone|null): void { + private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void { const userLeftMessage = new UserLeftZoneMessage(); userLeftMessage.setUserid(userId); userLeftMessage.setTozone(this.toProtoZone(newZone)); @@ -394,7 +406,7 @@ export class SocketManager { emitZoneMessage(subMessage, client); } - private toProtoZone(zone: Zone|null): ProtoZone|undefined { + private toProtoZone(zone: Zone | null): ProtoZone | undefined { if (zone !== null) { const zoneMessage = new ProtoZone(); zoneMessage.setX(zone.x); @@ -405,7 +417,6 @@ export class SocketManager { } private joinWebRtcRoom(user: User, group: Group) { - for (const otherUser of group.getUsers()) { if (user === otherUser) { continue; @@ -416,8 +427,8 @@ export class SocketManager { webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setInitiator(true); - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); webrtcStartMessage1.setWebrtcusername(username); webrtcStartMessage1.setWebrtcpassword(password); } @@ -426,16 +437,16 @@ export class SocketManager { serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); //if (!user.socket.disconnecting) { - user.socket.write(serverToClientMessage1); - //console.log('Sending webrtcstart initiator to '+user.socket.userId) + user.socket.write(serverToClientMessage1); + //console.log('Sending webrtcstart initiator to '+user.socket.userId) //} const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setInitiator(false); - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcStartMessage2.setWebrtcusername(username); webrtcStartMessage2.setWebrtcpassword(password); } @@ -444,10 +455,9 @@ export class SocketManager { serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage2); - //console.log('Sending webrtcstart to '+otherUser.socket.userId) + otherUser.socket.write(serverToClientMessage2); + //console.log('Sending webrtcstart to '+otherUser.socket.userId) //} - } } @@ -456,17 +466,17 @@ export class SocketManager { * and the Coturn server. * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey` */ - private getTURNCredentials(name: string, secret: string): {username: string, password: string} { - const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours - const username = [unixTimeStamp, name].join(':'); - const hmac = crypto.createHmac('sha1', secret); - hmac.setEncoding('base64'); + private getTURNCredentials(name: string, secret: string): { username: string; password: string } { + const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours + const username = [unixTimeStamp, name].join(":"); + const hmac = crypto.createHmac("sha1", secret); + hmac.setEncoding("base64"); hmac.write(username); hmac.end(); const password = hmac.read(); return { username: username, - password: password + password: password, }; } @@ -489,10 +499,9 @@ export class SocketManager { serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage1); + otherUser.socket.write(serverToClientMessage1); //} - const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); webrtcDisconnectMessage2.setUserid(otherUser.id); @@ -500,7 +509,7 @@ export class SocketManager { serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); //if (!user.socket.disconnecting) { - user.socket.write(serverToClientMessage2); + user.socket.write(serverToClientMessage2); //} } } @@ -517,40 +526,41 @@ export class SocketManager { console.error('An error occurred on "emitPlayGlobalMessage" event'); console.error(e); } - } public getWorlds(): Map { return this.rooms; } - public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) { const room = queryJitsiJwtMessage.getJitsiroom(); const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead. - if (SECRET_JITSI_KEY === '') { - throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); + if (SECRET_JITSI_KEY === "") { + throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi."); } // Let's see if the current client has const isAdmin = user.tags.includes(tag); - const jwt = Jwt.sign({ - "aud": "jitsi", - "iss": JITSI_ISS, - "sub": JITSI_URL, - "room": room, - "moderator": isAdmin - }, SECRET_JITSI_KEY, { - expiresIn: '1d', - algorithm: "HS256", - header: - { - "alg": "HS256", - "typ": "JWT" - } - }); + const jwt = Jwt.sign( + { + aud: "jitsi", + iss: JITSI_ISS, + sub: JITSI_URL, + room: room, + moderator: isAdmin, + }, + SECRET_JITSI_KEY, + { + expiresIn: "1d", + algorithm: "HS256", + header: { + alg: "HS256", + typ: "JWT", + }, + } + ); const sendJitsiJwtMessage = new SendJitsiJwtMessage(); sendJitsiJwtMessage.setJitsiroom(room); @@ -562,7 +572,7 @@ export class SocketManager { user.socket.write(serverToClientMessage); } - public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){ + public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(sendUserMessageToSend.getMessage()); sendUserMessage.setType(sendUserMessageToSend.getType()); @@ -572,7 +582,7 @@ export class SocketManager { user.socket.write(serverToClientMessage); } - public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){ + public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) { const banUserMessage = new BanUserMessage(); banUserMessage.setMessage(banUserMessageToSend.getMessage()); banUserMessage.setType(banUserMessageToSend.getType()); @@ -592,7 +602,7 @@ export class SocketManager { public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In addZoneListener, could not find room with id '" + roomId + "'"); + console.error("In addZoneListener, could not find room with id '" + roomId + "'"); return; } @@ -636,7 +646,7 @@ export class SocketManager { removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) { const room = this.rooms.get(roomId); if (!room) { - console.error("In removeZoneListener, could not find room with id '" + roomId + "'"); + console.error("In removeZoneListener, could not find room with id '" + roomId + "'"); return; } @@ -651,7 +661,7 @@ export class SocketManager { return room; } - public leaveAdminRoom(room: GameRoom, admin: Admin){ + public leaveAdminRoom(room: GameRoom, admin: Admin) { room.adminLeave(admin); if (room.isEmpty()) { this.rooms.delete(room.roomId); @@ -663,19 +673,27 @@ export class SocketManager { public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In sendAdminMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } const recipient = room.getUserByUuid(recipientUuid); if (recipient === undefined) { - console.error("In sendAdminMessage, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminMessage, could not find user with id '" + + recipientUuid + + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?" + ); return; } const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('ban'); //todo: is the type correct? + sendUserMessage.setType("ban"); //todo: is the type correct? const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setSendusermessage(sendUserMessage); @@ -686,13 +704,21 @@ export class SocketManager { public banUser(roomId: string, recipientUuid: string, message: string): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In banUser, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In banUser, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } const recipient = room.getUserByUuid(recipientUuid); if (recipient === undefined) { - console.error("In banUser, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); + console.error( + "In banUser, could not find user with id '" + + recipientUuid + + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?" + ); return; } @@ -701,7 +727,7 @@ export class SocketManager { const banUserMessage = new BanUserMessage(); banUserMessage.setMessage(message); - banUserMessage.setType('banned'); + banUserMessage.setType("banned"); const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setBanusermessage(banUserMessage); @@ -711,19 +737,22 @@ export class SocketManager { recipient.socket.end(); } - sendAdminRoomMessage(roomId: string, message: string) { const room = this.rooms.get(roomId); if (!room) { //todo: this should cause the http call to return a 500 - console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminRoomMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } room.getUsers().forEach((recipient) => { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('message'); + sendUserMessage.setType("message"); const clientMessage = new ServerToClientMessage(); clientMessage.setSendusermessage(sendUserMessage); @@ -732,14 +761,18 @@ export class SocketManager { }); } - dispatchWorlFullWarning(roomId: string,): void { + dispatchWorlFullWarning(roomId: string): void { const room = this.rooms.get(roomId); if (!room) { //todo: this should cause the http call to return a 500 - console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminRoomMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } - + room.getUsers().forEach((recipient) => { const worldFullMessage = new WorldFullWarningMessage(); @@ -750,17 +783,17 @@ export class SocketManager { }); } - dispatchRoomRefresh(roomId: string,): void { + dispatchRoomRefresh(roomId: string): void { const room = this.rooms.get(roomId); if (!room) { return; } - + const versionNumber = room.incrementVersion(); room.getUsers().forEach((recipient) => { const worldFullMessage = new RefreshRoomMessage(); - worldFullMessage.setRoomid(roomId) - worldFullMessage.setVersionnumber(versionNumber) + worldFullMessage.setRoomid(roomId); + worldFullMessage.setVersionnumber(versionNumber); const clientMessage = new ServerToClientMessage(); clientMessage.setRefreshroommessage(worldFullMessage); diff --git a/pusher/src/App.ts b/pusher/src/App.ts index 7a272404..81aed045 100644 --- a/pusher/src/App.ts +++ b/pusher/src/App.ts @@ -1,11 +1,11 @@ // lib/app.ts -import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..." -import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..." -import {MapController} from "./Controller/MapController"; -import {PrometheusController} from "./Controller/PrometheusController"; -import {DebugController} from "./Controller/DebugController"; -import {App as uwsApp} from "./Server/sifrr.server"; -import {AdminController} from "./Controller/AdminController"; +import { IoSocketController } from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..." +import { AuthenticateController } from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..." +import { MapController } from "./Controller/MapController"; +import { PrometheusController } from "./Controller/PrometheusController"; +import { DebugController } from "./Controller/DebugController"; +import { App as uwsApp } from "./Server/sifrr.server"; +import { AdminController } from "./Controller/AdminController"; class App { public app: uwsApp; diff --git a/pusher/src/Controller/AdminController.ts b/pusher/src/Controller/AdminController.ts index 74d4e792..ec1bd067 100644 --- a/pusher/src/Controller/AdminController.ts +++ b/pusher/src/Controller/AdminController.ts @@ -1,19 +1,21 @@ -import {BaseController} from "./BaseController"; -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {apiClientRepository} from "../Services/ApiClientRepository"; -import {AdminRoomMessage, WorldFullWarningToRoomMessage, RefreshRoomPromptMessage} from "../Messages/generated/messages_pb"; +import { BaseController } from "./BaseController"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { apiClientRepository } from "../Services/ApiClientRepository"; +import { + AdminRoomMessage, + WorldFullWarningToRoomMessage, + RefreshRoomPromptMessage, +} from "../Messages/generated/messages_pb"; - -export class AdminController extends BaseController{ - - constructor(private App : TemplatedApp) { +export class AdminController extends BaseController { + constructor(private App: TemplatedApp) { super(); this.App = App; this.receiveGlobalMessagePrompt(); this.receiveRoomEditionPrompt(); } - + receiveRoomEditionPrompt() { this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -23,25 +25,25 @@ export class AdminController extends BaseController{ // eslint-disable-next-line @typescript-eslint/no-misused-promises this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => { res.onAborted(() => { - console.warn('/message request was aborted'); - }) + console.warn("/message request was aborted"); + }); - const token = req.getHeader('admin-token'); + const token = req.getHeader("admin-token"); const body = await res.json(); if (token !== ADMIN_API_TOKEN) { - console.error('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.error("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } try { - if (typeof body.roomId !== 'string') { - throw 'Incorrect roomId parameter' + if (typeof body.roomId !== "string") { + throw "Incorrect roomId parameter"; } const roomId: string = body.roomId; - await apiClientRepository.getClient(roomId).then((roomClient) =>{ + await apiClientRepository.getClient(roomId).then((roomClient) => { return new Promise((res, rej) => { const roomMessage = new RefreshRoomPromptMessage(); roomMessage.setRoomid(roomId); @@ -57,12 +59,10 @@ export class AdminController extends BaseController{ } res.writeStatus("200"); - res.end('ok'); - - + res.end("ok"); }); } - + receiveGlobalMessagePrompt() { this.App.options("/message", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -71,59 +71,57 @@ export class AdminController extends BaseController{ // eslint-disable-next-line @typescript-eslint/no-misused-promises this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => { - res.onAborted(() => { - console.warn('/message request was aborted'); - }) + console.warn("/message request was aborted"); + }); - - const token = req.getHeader('admin-token'); + const token = req.getHeader("admin-token"); const body = await res.json(); - + if (token !== ADMIN_API_TOKEN) { - console.error('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.error("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } try { - if (typeof body.text !== 'string') { - throw 'Incorrect text parameter' + if (typeof body.text !== "string") { + throw "Incorrect text parameter"; } - if (body.type !== 'capacity' && body.type !== 'message') { - throw 'Incorrect type parameter' + if (body.type !== "capacity" && body.type !== "message") { + throw "Incorrect type parameter"; } - if (!body.targets || typeof body.targets !== 'object') { - throw 'Incorrect targets parameter' + if (!body.targets || typeof body.targets !== "object") { + throw "Incorrect targets parameter"; } const text: string = body.text; const type: string = body.type; const targets: string[] = body.targets; - await Promise.all(targets.map((roomId) => { - return apiClientRepository.getClient(roomId).then((roomClient) =>{ - return new Promise((res, rej) => { - if (type === 'message') { - const roomMessage = new AdminRoomMessage(); - roomMessage.setMessage(text); - roomMessage.setRoomid(roomId); + await Promise.all( + targets.map((roomId) => { + return apiClientRepository.getClient(roomId).then((roomClient) => { + return new Promise((res, rej) => { + if (type === "message") { + const roomMessage = new AdminRoomMessage(); + roomMessage.setMessage(text); + roomMessage.setRoomid(roomId); - roomClient.sendAdminMessageToRoom(roomMessage, (err) => { - err ? rej(err) : res(); - }); - } else if (type === 'capacity') { - const roomMessage = new WorldFullWarningToRoomMessage(); - roomMessage.setRoomid(roomId); - - roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => { - err ? rej(err) : res(); - }); - } + roomClient.sendAdminMessageToRoom(roomMessage, (err) => { + err ? rej(err) : res(); + }); + } else if (type === "capacity") { + const roomMessage = new WorldFullWarningToRoomMessage(); + roomMessage.setRoomid(roomId); + roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => { + err ? rej(err) : res(); + }); + } + }); }); - }); - })); - + }) + ); } catch (err) { this.errorToResponse(err, res); return; @@ -131,7 +129,7 @@ export class AdminController extends BaseController{ res.writeStatus("200"); this.addCorsHeaders(res); - res.end('ok'); + res.end("ok"); }); } } diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 317848c0..3012e275 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -1,17 +1,16 @@ -import { v4 } from 'uuid'; -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {BaseController} from "./BaseController"; -import {adminApi} from "../Services/AdminApi"; -import {jwtTokenManager} from "../Services/JWTTokenManager"; -import {parse} from "query-string"; +import { v4 } from "uuid"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { BaseController } from "./BaseController"; +import { adminApi } from "../Services/AdminApi"; +import { jwtTokenManager } from "../Services/JWTTokenManager"; +import { parse } from "query-string"; export interface TokenInterface { - userUuid: string + userUuid: string; } export class AuthenticateController extends BaseController { - - constructor(private App : TemplatedApp) { + constructor(private App: TemplatedApp) { super(); this.register(); this.verify(); @@ -19,7 +18,7 @@ export class AuthenticateController extends BaseController { } //Try to login with an admin token - private register(){ + private register() { this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -29,15 +28,15 @@ export class AuthenticateController extends BaseController { this.App.post("/register", (res: HttpResponse, req: HttpRequest) => { (async () => { res.onAborted(() => { - console.warn('Login request was aborted'); - }) + console.warn("Login request was aborted"); + }); const param = await res.json(); //todo: what to do if the organizationMemberToken is already used? - const organizationMemberToken:string|null = param.organizationMemberToken; + const organizationMemberToken: string | null = param.organizationMemberToken; try { - if (typeof organizationMemberToken != 'string') throw new Error('No organization token'); + if (typeof organizationMemberToken != "string") throw new Error("No organization token"); const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); const userUuid = data.userUuid; const organizationSlug = data.organizationSlug; @@ -49,28 +48,26 @@ export class AuthenticateController extends BaseController { const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - authToken, - userUuid, - organizationSlug, - worldSlug, - roomSlug, - mapUrlStart, - organizationMemberToken, - textures - })); - + res.end( + JSON.stringify({ + authToken, + userUuid, + organizationSlug, + worldSlug, + roomSlug, + mapUrlStart, + organizationMemberToken, + textures, + }) + ); } catch (e) { this.errorToResponse(e, res); } - - })(); }); - } - private verify(){ + private verify() { this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -82,50 +79,55 @@ export class AuthenticateController extends BaseController { const query = parse(req.getQuery()); res.onAborted(() => { - console.warn('verify request was aborted'); - }) + console.warn("verify request was aborted"); + }); try { await jwtTokenManager.getUserUuidFromToken(query.token as string); } catch (e) { res.writeStatus("400 Bad Request"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - "success": false, - "message": "Invalid JWT token" - })); + res.end( + JSON.stringify({ + success: false, + message: "Invalid JWT token", + }) + ); return; } res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - "success": true - })); + res.end( + JSON.stringify({ + success: true, + }) + ); })(); }); - } //permit to login on application. Return token to connect on Websocket IO. - private anonymLogin(){ + private anonymLogin() { this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); res.end(); }); - this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { res.onAborted(() => { - console.warn('Login request was aborted'); - }) + console.warn("Login request was aborted"); + }); const userUuid = v4(); const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - authToken, - userUuid, - })); + res.end( + JSON.stringify({ + authToken, + userUuid, + }) + ); }); } } diff --git a/pusher/src/Controller/BaseController.ts b/pusher/src/Controller/BaseController.ts index 91882138..ce378a55 100644 --- a/pusher/src/Controller/BaseController.ts +++ b/pusher/src/Controller/BaseController.ts @@ -1,11 +1,10 @@ -import {HttpResponse} from "uWebSockets.js"; - +import { HttpResponse } from "uWebSockets.js"; export class BaseController { protected addCorsHeaders(res: HttpResponse): void { - res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept'); - res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); - res.writeHeader('access-control-allow-origin', '*'); + res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept"); + res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE"); + res.writeHeader("access-control-allow-origin", "*"); } /** @@ -16,23 +15,23 @@ export class BaseController { if (e && e.message) { let url = e?.config?.url; if (url !== undefined) { - url = ' for URL: '+url; + url = " for URL: " + url; } else { - url = ''; + url = ""; } - console.error('ERROR: '+e.message+url); - } else if (typeof(e) === 'string') { + console.error("ERROR: " + e.message + url); + } else if (typeof e === "string") { console.error(e); } if (e.stack) { console.error(e.stack); } if (e.response) { - res.writeStatus(e.response.status+" "+e.response.statusText); + res.writeStatus(e.response.status + " " + e.response.statusText); this.addCorsHeaders(res); - res.end("An error occurred: "+e.response.status+" "+e.response.statusText); + res.end("An error occurred: " + e.response.status + " " + e.response.statusText); } else { - res.writeStatus("500 Internal Server Error") + res.writeStatus("500 Internal Server Error"); this.addCorsHeaders(res); res.end("An error occurred"); } diff --git a/pusher/src/Controller/DebugController.ts b/pusher/src/Controller/DebugController.ts index af2db139..0b0d188b 100644 --- a/pusher/src/Controller/DebugController.ts +++ b/pusher/src/Controller/DebugController.ts @@ -1,45 +1,46 @@ -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {IoSocketController} from "_Controller/IoSocketController"; -import {stringify} from "circular-json"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -import { parse } from 'query-string'; -import {App} from "../Server/sifrr.server"; -import {socketManager} from "../Services/SocketManager"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { IoSocketController } from "_Controller/IoSocketController"; +import { stringify } from "circular-json"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +import { parse } from "query-string"; +import { App } from "../Server/sifrr.server"; +import { socketManager } from "../Services/SocketManager"; export class DebugController { - constructor(private App : App) { + constructor(private App: App) { this.getDump(); } - - getDump(){ + getDump() { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { const query = parse(req.getQuery()); if (query.token !== ADMIN_API_TOKEN) { - return res.status(401).send('Invalid token sent!'); + return res.status(401).send("Invalid token sent!"); } - return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( - socketManager.getWorlds(), - (key: unknown, value: unknown) => { - if(value instanceof Map) { - const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any - for (const [mapKey, mapValue] of value.entries()) { - obj[mapKey] = mapValue; - } - return obj; - } else if(value instanceof Set) { + return res + .writeStatus("200 OK") + .writeHeader("Content-Type", "application/json") + .end( + stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { + if (value instanceof Map) { + const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any + for (const [mapKey, mapValue] of value.entries()) { + obj[mapKey] = mapValue; + } + return obj; + } else if (value instanceof Set) { const obj: Array = []; for (const [setKey, setValue] of value.entries()) { obj.push(setValue); } return obj; - } else { - return value; - } - } - )); + } else { + return value; + } + }) + ); }); } } diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index b2079953..1af9d917 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -1,6 +1,6 @@ -import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." -import {GameRoomPolicyTypes} from "../Model/PusherRoom"; -import {PointInterface} from "../Model/Websocket/PointInterface"; +import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." +import { GameRoomPolicyTypes } from "../Model/PusherRoom"; +import { PointInterface } from "../Model/Websocket/PointInterface"; import { SetPlayerDetailsMessage, SubMessage, @@ -18,17 +18,17 @@ import { CompanionMessage, EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {UserMovesMessage} from "../Messages/generated/messages_pb"; -import {TemplatedApp} from "uWebSockets.js" -import {parse} from "query-string"; -import {jwtTokenManager} from "../Services/JWTTokenManager"; -import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; -import {SocketManager, socketManager} from "../Services/SocketManager"; -import {emitInBatch} from "../Services/IoSocketHelpers"; -import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable"; -import {Zone} from "_Model/Zone"; -import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; -import {v4} from "uuid"; +import { UserMovesMessage } from "../Messages/generated/messages_pb"; +import { TemplatedApp } from "uWebSockets.js"; +import { parse } from "query-string"; +import { jwtTokenManager } from "../Services/JWTTokenManager"; +import { adminApi, CharacterTexture, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; +import { SocketManager, socketManager } from "../Services/SocketManager"; +import { emitInBatch } from "../Services/IoSocketHelpers"; +import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable"; +import { Zone } from "_Model/Zone"; +import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; +import { v4 } from "uuid"; export class IoSocketController { private nextUserId: number = 1; @@ -39,32 +39,29 @@ export class IoSocketController { } adminRoomSocket() { - this.app.ws('/admin/rooms', { + this.app.ws("/admin/rooms", { upgrade: (res, req, context) => { const query = parse(req.getQuery()); - const websocketKey = req.getHeader('sec-websocket-key'); - const websocketProtocol = req.getHeader('sec-websocket-protocol'); - const websocketExtensions = req.getHeader('sec-websocket-extensions'); + const websocketKey = req.getHeader("sec-websocket-key"); + const websocketProtocol = req.getHeader("sec-websocket-protocol"); + const websocketExtensions = req.getHeader("sec-websocket-extensions"); const token = query.token; if (token !== ADMIN_API_TOKEN) { - console.log('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.log("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } const roomId = query.roomId; - if (typeof roomId !== 'string') { - console.error('Received') - res.writeStatus("400 Bad Request").end('Missing room id'); + if (typeof roomId !== "string") { + console.error("Received"); + res.writeStatus("400 Bad Request").end("Missing room id"); return; } - res.upgrade( - {roomId}, - websocketKey, websocketProtocol, websocketExtensions, context, - ); + res.upgrade({ roomId }, websocketKey, websocketProtocol, websocketExtensions, context); }, open: (ws) => { - console.log('Admin socket connect for room: '+ws.roomId); + console.log("Admin socket connect for room: " + ws.roomId); ws.disconnecting = false; socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string); @@ -74,24 +71,34 @@ export class IoSocketController { const roomId = ws.roomId as string; //TODO refactor message type and data - const message: {event: string, message: {type: string, message: unknown, userUuid: string}} = + const message: { event: string; message: { type: string; message: unknown; userUuid: string } } = JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer))); - if(message.event === 'user-message') { - const messageToEmit = (message.message as { message: string, type: string, userUuid: string }); - if(messageToEmit.type === 'banned'){ - socketManager.emitBan(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string); + if (message.event === "user-message") { + const messageToEmit = message.message as { message: string; type: string; userUuid: string }; + if (messageToEmit.type === "banned") { + socketManager.emitBan( + messageToEmit.userUuid, + messageToEmit.message, + messageToEmit.type, + ws.roomId as string + ); } - if(messageToEmit.type === 'ban') { - socketManager.emitSendUserMessage(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string); + if (messageToEmit.type === "ban") { + socketManager.emitSendUserMessage( + messageToEmit.userUuid, + messageToEmit.message, + messageToEmit.type, + ws.roomId as string + ); } } - }catch (err) { + } catch (err) { console.error(err); } }, close: (ws, code, message) => { - const Client = (ws as ExAdminSocketInterface); + const Client = ws as ExAdminSocketInterface; try { Client.disconnecting = true; socketManager.leaveAdminRoom(Client); @@ -99,12 +106,12 @@ export class IoSocketController { console.error('An error occurred on admin "disconnect"'); console.error(e); } - } - }) + }, + }); } ioConnection() { - this.app.ws('/room', { + this.app.ws("/room", { /* Options */ //compression: uWS.SHARED_COMPRESSOR, idleTimeout: SOCKET_IDLE_TIMER, @@ -114,7 +121,7 @@ export class IoSocketController { upgrade: (res, req, context) => { (async () => { /* Keep track of abortions */ - const upgradeAborted = {aborted: false}; + const upgradeAborted = { aborted: false }; res.onAborted(() => { /* We can simply signal that we were aborted */ @@ -123,15 +130,15 @@ export class IoSocketController { const url = req.getUrl(); const query = parse(req.getQuery()); - const websocketKey = req.getHeader('sec-websocket-key'); - const websocketProtocol = req.getHeader('sec-websocket-protocol'); - const websocketExtensions = req.getHeader('sec-websocket-extensions'); - const IPAddress = req.getHeader('x-forwarded-for'); + const websocketKey = req.getHeader("sec-websocket-key"); + const websocketProtocol = req.getHeader("sec-websocket-protocol"); + const websocketExtensions = req.getHeader("sec-websocket-extensions"); + const IPAddress = req.getHeader("x-forwarded-for"); const roomId = query.roomId; try { - if (typeof roomId !== 'string') { - throw new Error('Undefined room ID: '); + if (typeof roomId !== "string") { + throw new Error("Undefined room ID: "); } const token = query.token; @@ -143,62 +150,69 @@ export class IoSocketController { const right = Number(query.right); const name = query.name; - let companion: CompanionMessage|undefined = undefined; + let companion: CompanionMessage | undefined = undefined; - if (typeof query.companion === 'string') { + if (typeof query.companion === "string") { companion = new CompanionMessage(); companion.setName(query.companion); } - if (typeof name !== 'string') { - throw new Error('Expecting name'); + if (typeof name !== "string") { + throw new Error("Expecting name"); } - if (name === '') { - throw new Error('No empty name'); + if (name === "") { + throw new Error("No empty name"); } let characterLayers = query.characterLayers; if (characterLayers === null) { - throw new Error('Expecting skin'); + throw new Error("Expecting skin"); } - if (typeof characterLayers === 'string') { - characterLayers = [ characterLayers ]; + if (typeof characterLayers === "string") { + characterLayers = [characterLayers]; } const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId); let memberTags: string[] = []; - let memberVisitCardUrl: string|null = null; + let memberVisitCardUrl: string | null = null; let memberMessages: unknown; let memberTextures: CharacterTexture[] = []; const room = await socketManager.getOrCreateRoom(roomId); if (ADMIN_API_URL) { try { - let userData : FetchMemberDataByUuidResponse = { + let userData: FetchMemberDataByUuidResponse = { uuid: v4(), tags: [], visitCardUrl: null, textures: [], messages: [], - anonymous: true + anonymous: true, }; try { userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId); - }catch (err){ + } catch (err) { if (err?.response?.status == 404) { // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! - console.warn('Cannot find user with uuid "'+userUuid+'". Performing an anonymous login instead.'); - } else if(err?.response?.status == 403) { + console.warn( + 'Cannot find user with uuid "' + + userUuid + + '". Performing an anonymous login instead.' + ); + } else if (err?.response?.status == 403) { // If we get an HTTP 403, the world is full. We need to broadcast a special error to the client. // we finish immediately the upgrade then we will close the socket as soon as it starts opening. - return res.upgrade({ - rejected: true, - message: err?.response?.data.message, - status: err?.response?.status - }, websocketKey, - websocketProtocol, - websocketExtensions, - context); - }else{ + return res.upgrade( + { + rejected: true, + message: err?.response?.data.message, + status: err?.response?.status, + }, + websocketKey, + websocketProtocol, + websocketExtensions, + context + ); + } else { throw err; } } @@ -206,21 +220,30 @@ export class IoSocketController { memberTags = userData.tags; memberVisitCardUrl = userData.visitCardUrl; memberTextures = userData.textures; - if (!room.public && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags))) { - throw new Error('Insufficient privileges to access this room') + if ( + !room.public && + room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && + (userData.anonymous === true || !room.canAccess(memberTags)) + ) { + throw new Error("Insufficient privileges to access this room"); } - if (!room.public && room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && userData.anonymous === true) { - throw new Error('Use the login URL to connect') + if ( + !room.public && + room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && + userData.anonymous === true + ) { + throw new Error("Use the login URL to connect"); } } catch (e) { - console.log('access not granted for user '+userUuid+' and room '+roomId); + console.log("access not granted for user " + userUuid + " and room " + roomId); console.error(e); - throw new Error('User cannot access this world') + throw new Error("User cannot access this world"); } } // Generate characterLayers objects from characterLayers string[] - const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures); + const characterLayerObjs: CharacterLayer[] = + SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures); if (upgradeAborted.aborted) { console.log("Ouch! Client disconnected before we could upgrade it!"); @@ -229,7 +252,8 @@ export class IoSocketController { } /* This immediately calls open handler, you must not use res after this call */ - res.upgrade({ + res.upgrade( + { // Data passed here is accessible on the "websocket" socket object. url, token, @@ -246,22 +270,22 @@ export class IoSocketController { position: { x: x, y: y, - direction: 'down', - moving: false + direction: "down", + moving: false, } as PointInterface, viewport: { top, right, bottom, - left - } + left, + }, }, /* Spell these correctly */ websocketKey, websocketProtocol, websocketExtensions, - context); - + context + ); } catch (e) { /*if (e instanceof Error) { console.log(e.message); @@ -269,23 +293,26 @@ export class IoSocketController { } else { res.writeStatus("500 Internal Server Error").end('An error occurred'); }*/ - return res.upgrade({ - rejected: true, - message: e.message ? e.message : '500 Internal Server Error' - }, websocketKey, - websocketProtocol, - websocketExtensions, - context); + return res.upgrade( + { + rejected: true, + message: e.message ? e.message : "500 Internal Server Error", + }, + websocketKey, + websocketProtocol, + websocketExtensions, + context + ); } })(); }, /* Handlers */ open: (ws) => { - if(ws.rejected === true) { + if (ws.rejected === true) { //FIX ME to use status code - if(ws.message === 'World is full'){ + if (ws.message === "World is full") { socketManager.emitWorldFullMessage(ws); - }else{ + } else { socketManager.emitConnexionErrorMessage(ws, ws.message as string); } ws.close(); @@ -299,7 +326,7 @@ export class IoSocketController { //get data information and show messages if (client.messages && Array.isArray(client.messages)) { client.messages.forEach((c: unknown) => { - const messageToSend = c as { type: string, message: string }; + const messageToSend = c as { type: string; message: string }; const sendUserMessage = new SendUserMessage(); sendUserMessage.setType(messageToSend.type); @@ -323,33 +350,48 @@ export class IoSocketController { } else if (message.hasUsermovesmessage()) { socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); } else if (message.hasSetplayerdetailsmessage()) { - socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage); + socketManager.handleSetPlayerDetails( + client, + message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage + ); } else if (message.hasSilentmessage()) { socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitVideo( + client, + message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitScreenSharing( + client, + message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasPlayglobalmessage()) { socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage); - } else if (message.hasReportplayermessage()){ + } else if (message.hasReportplayermessage()) { socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); - } else if (message.hasQueryjitsijwtmessage()){ - socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); - } else if (message.hasEmotepromptmessage()){ - socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage); + } else if (message.hasQueryjitsijwtmessage()) { + socketManager.handleQueryJitsiJwtMessage( + client, + message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage + ); + } else if (message.hasEmotepromptmessage()) { + socketManager.handleEmotePromptMessage( + client, + message.getEmotepromptmessage() as EmotePromptMessage + ); } - /* Ok is false if backpressure was built up, wait for drain */ + /* Ok is false if backpressure was built up, wait for drain */ //let ok = ws.send(message, isBinary); }, drain: (ws) => { - console.log('WebSocket backpressure: ' + ws.getBufferedAmount()); + console.log("WebSocket backpressure: " + ws.getBufferedAmount()); }, close: (ws, code, message) => { - const Client = (ws as ExSocketInterface); + const Client = ws as ExSocketInterface; try { Client.disconnecting = true; //leave room @@ -358,13 +400,13 @@ export class IoSocketController { console.error('An error occurred on "disconnect"'); console.error(e); } - } - }) + }, + }); } //eslint-disable-next-line @typescript-eslint/no-explicit-any private initClient(ws: any): ExSocketInterface { - const client : ExSocketInterface = ws; + const client: ExSocketInterface = ws; client.userId = this.nextUserId; this.nextUserId++; client.userUuid = ws.userUuid; @@ -374,7 +416,7 @@ export class IoSocketController { client.batchTimeout = null; client.emitInBatch = (payload: SubMessage): void => { emitInBatch(client, payload); - } + }; client.disconnecting = false; client.messages = ws.messages; diff --git a/pusher/src/Controller/MapController.ts b/pusher/src/Controller/MapController.ts index 1ce04265..1df828d4 100644 --- a/pusher/src/Controller/MapController.ts +++ b/pusher/src/Controller/MapController.ts @@ -1,18 +1,15 @@ -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {BaseController} from "./BaseController"; -import {parse} from "query-string"; -import {adminApi} from "../Services/AdminApi"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { BaseController } from "./BaseController"; +import { parse } from "query-string"; +import { adminApi } from "../Services/AdminApi"; - -export class MapController extends BaseController{ - - constructor(private App : TemplatedApp) { +export class MapController extends BaseController { + constructor(private App: TemplatedApp) { super(); this.App = App; this.getMapUrl(); } - // Returns a map mapping map name to file name of the map getMapUrl() { this.App.options("/map", (res: HttpResponse, req: HttpRequest) => { @@ -22,29 +19,28 @@ export class MapController extends BaseController{ }); this.App.get("/map", (res: HttpResponse, req: HttpRequest) => { - res.onAborted(() => { - console.warn('/map request was aborted'); - }) + console.warn("/map request was aborted"); + }); const query = parse(req.getQuery()); - if (typeof query.organizationSlug !== 'string') { - console.error('Expected organizationSlug parameter'); + if (typeof query.organizationSlug !== "string") { + console.error("Expected organizationSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected organizationSlug parameter"); return; } - if (typeof query.worldSlug !== 'string') { - console.error('Expected worldSlug parameter'); + if (typeof query.worldSlug !== "string") { + console.error("Expected worldSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected worldSlug parameter"); return; } - if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { - console.error('Expected only one roomSlug parameter'); + if (typeof query.roomSlug !== "string" && query.roomSlug !== undefined) { + console.error("Expected only one roomSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected only one roomSlug parameter"); @@ -53,7 +49,11 @@ export class MapController extends BaseController{ (async () => { try { - const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined); + const mapDetails = await adminApi.fetchMapDetails( + query.organizationSlug as string, + query.worldSlug as string, + query.roomSlug as string | undefined + ); res.writeStatus("200 OK"); this.addCorsHeaders(res); @@ -62,7 +62,6 @@ export class MapController extends BaseController{ this.errorToResponse(e, res); } })(); - }); } } diff --git a/pusher/src/Controller/PrometheusController.ts b/pusher/src/Controller/PrometheusController.ts index e854cf43..3ab3d33f 100644 --- a/pusher/src/Controller/PrometheusController.ts +++ b/pusher/src/Controller/PrometheusController.ts @@ -1,7 +1,7 @@ -import {App} from "../Server/sifrr.server"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -const register = require('prom-client').register; -const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; +import { App } from "../Server/sifrr.server"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +const register = require("prom-client").register; +const collectDefaultMetrics = require("prom-client").collectDefaultMetrics; export class PrometheusController { constructor(private App: App) { @@ -14,7 +14,7 @@ export class PrometheusController { } private metrics(res: HttpResponse, req: HttpRequest): void { - res.writeHeader('Content-Type', register.contentType); + res.writeHeader("Content-Type", register.contentType); res.end(register.metrics()); } } diff --git a/pusher/src/Enum/EnvironmentVariable.ts b/pusher/src/Enum/EnvironmentVariable.ts index 5b3ec9c4..be974697 100644 --- a/pusher/src/Enum/EnvironmentVariable.ts +++ b/pusher/src/Enum/EnvironmentVariable.ts @@ -1,16 +1,16 @@ const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY"; const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; -const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; -const API_URL = process.env.API_URL || ''; -const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; -const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600; +const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; +const API_URL = process.env.API_URL || ""; +const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; +const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || "") || 600; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; -const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; -const JITSI_ISS = process.env.JITSI_ISS || ''; -const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; -const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || '8080') || 8080 +const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; +const JITSI_ISS = process.env.JITSI_ISS || ""; +const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; +const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080; export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed export { @@ -26,5 +26,5 @@ export { JITSI_URL, JITSI_ISS, SECRET_JITSI_KEY, - PUSHER_HTTP_PORT -} + PUSHER_HTTP_PORT, +}; diff --git a/pusher/src/Model/Movable.ts b/pusher/src/Model/Movable.ts index 173db0ae..ca586b7c 100644 --- a/pusher/src/Model/Movable.ts +++ b/pusher/src/Model/Movable.ts @@ -1,8 +1,8 @@ -import {PositionInterface} from "_Model/PositionInterface"; +import { PositionInterface } from "_Model/PositionInterface"; /** * A physical object that can be placed into a Zone */ export interface Movable { - getPosition(): PositionInterface + getPosition(): PositionInterface; } diff --git a/pusher/src/Model/PositionDispatcher.ts b/pusher/src/Model/PositionDispatcher.ts index 1150394b..594328e3 100644 --- a/pusher/src/Model/PositionDispatcher.ts +++ b/pusher/src/Model/PositionDispatcher.ts @@ -8,9 +8,9 @@ * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * number of players around the current player. */ -import {Zone, ZoneEventListener} from "./Zone"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import { Zone, ZoneEventListener } from "./Zone"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; //import Debug from "debug"; //const debug = Debug('positiondispatcher'); @@ -21,19 +21,22 @@ interface ZoneDescriptor { } export class PositionDispatcher { - // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) private zones: Zone[][] = []; - constructor(public readonly roomId: string, private zoneWidth: number, private zoneHeight: number, private socketListener: ZoneEventListener) { - } + constructor( + public readonly roomId: string, + private zoneWidth: number, + private zoneHeight: number, + private socketListener: ZoneEventListener + ) {} private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), - } + }; } /** @@ -41,7 +44,7 @@ export class PositionDispatcher { */ public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void { if (viewport.left > viewport.right || viewport.top > viewport.bottom) { - console.warn('Invalid viewport received: ', viewport); + console.warn("Invalid viewport received: ", viewport); return; } @@ -57,8 +60,8 @@ export class PositionDispatcher { } } - const addedZones = [...newZones].filter(x => !oldZones.has(x)); - const removedZones = [...oldZones].filter(x => !newZones.has(x)); + const addedZones = [...newZones].filter((x) => !oldZones.has(x)); + const removedZones = [...oldZones].filter((x) => !newZones.has(x)); for (const zone of addedZones) { zone.startListening(socket); diff --git a/pusher/src/Model/PositionInterface.ts b/pusher/src/Model/PositionInterface.ts index d3b0dd47..65636759 100644 --- a/pusher/src/Model/PositionInterface.ts +++ b/pusher/src/Model/PositionInterface.ts @@ -1,4 +1,4 @@ export interface PositionInterface { - x: number, - y: number + x: number; + y: number; } diff --git a/pusher/src/Model/PusherRoom.ts b/pusher/src/Model/PusherRoom.ts index dcea5859..a49fce3e 100644 --- a/pusher/src/Model/PusherRoom.ts +++ b/pusher/src/Model/PusherRoom.ts @@ -1,9 +1,9 @@ -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; -import {PositionDispatcher} from "./PositionDispatcher"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; -import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; -import {arrayIntersect} from "../Services/ArrayHelper"; -import {ZoneEventListener} from "_Model/Zone"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; +import { PositionDispatcher } from "./PositionDispatcher"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; +import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; +import { arrayIntersect } from "../Services/ArrayHelper"; +import { ZoneEventListener } from "_Model/Zone"; export enum GameRoomPolicyTypes { ANONYMUS_POLICY = 1, @@ -17,13 +17,11 @@ export class PusherRoom { public tags: string[]; public policyType: GameRoomPolicyTypes; public readonly roomSlug: string; - public readonly worldSlug: string = ''; - public readonly organizationSlug: string = ''; + public readonly worldSlug: string = ""; + public readonly organizationSlug: string = ""; private versionNumber: number = 1; - constructor(public readonly roomId: string, - private socketListener: ZoneEventListener) - { + constructor(public readonly roomId: string, private socketListener: ZoneEventListener) { this.public = isRoomAnonymous(roomId); this.tags = []; this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; @@ -31,7 +29,7 @@ export class PusherRoom { if (this.public) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { - const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId); this.roomSlug = roomSlug; this.organizationSlug = organizationSlug; this.worldSlug = worldSlug; @@ -41,11 +39,11 @@ export class PusherRoom { this.positionNotifier = new PositionDispatcher(this.roomId, 320, 320, this.socketListener); } - public setViewport(socket : ExSocketInterface, viewport: ViewportInterface): void { + public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void { this.positionNotifier.setViewport(socket, viewport); } - public leave(socket : ExSocketInterface){ + public leave(socket: ExSocketInterface) { this.positionNotifier.removeViewport(socket); } diff --git a/pusher/src/Model/RoomIdentifier.ts b/pusher/src/Model/RoomIdentifier.ts index 3ac62bca..d1de8800 100644 --- a/pusher/src/Model/RoomIdentifier.ts +++ b/pusher/src/Model/RoomIdentifier.ts @@ -1,30 +1,30 @@ //helper functions to parse room IDs export const isRoomAnonymous = (roomID: string): boolean => { - if (roomID.startsWith('_/')) { + if (roomID.startsWith("_/")) { return true; - } else if(roomID.startsWith('@/')) { + } else if (roomID.startsWith("@/")) { return false; } else { - throw new Error('Incorrect room ID: '+roomID); + throw new Error("Incorrect room ID: " + roomID); } -} +}; export const extractRoomSlugPublicRoomId = (roomId: string): string => { - const idParts = roomId.split('/'); - if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); - return idParts.slice(2).join('/'); -} + const idParts = roomId.split("/"); + if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId); + return idParts.slice(2).join("/"); +}; export interface extractDataFromPrivateRoomIdResponse { organizationSlug: string; worldSlug: string; roomSlug: string; } export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { - const idParts = roomId.split('/'); - if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const idParts = roomId.split("/"); + if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId); const organizationSlug = idParts[1]; const worldSlug = idParts[2]; const roomSlug = idParts[3]; - return {organizationSlug, worldSlug, roomSlug} -} \ No newline at end of file + return { organizationSlug, worldSlug, roomSlug }; +}; diff --git a/pusher/src/Model/Websocket/ExAdminSocketInterface.ts b/pusher/src/Model/Websocket/ExAdminSocketInterface.ts index 1e03db6c..7599c82c 100644 --- a/pusher/src/Model/Websocket/ExAdminSocketInterface.ts +++ b/pusher/src/Model/Websocket/ExAdminSocketInterface.ts @@ -1,21 +1,22 @@ -import {PointInterface} from "./PointInterface"; -import {Identificable} from "./Identificable"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import { PointInterface } from "./PointInterface"; +import { Identificable } from "./Identificable"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; import { AdminPusherToBackMessage, BatchMessage, - PusherToBackMessage, ServerToAdminClientMessage, + PusherToBackMessage, + ServerToAdminClientMessage, ServerToClientMessage, - SubMessage + SubMessage, } from "../../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js" -import {CharacterTexture} from "../../Services/AdminApi"; -import {ClientDuplexStream} from "grpc"; -import {Zone} from "_Model/Zone"; +import { WebSocket } from "uWebSockets.js"; +import { CharacterTexture } from "../../Services/AdminApi"; +import { ClientDuplexStream } from "grpc"; +import { Zone } from "_Model/Zone"; export type AdminConnection = ClientDuplexStream; export interface ExAdminSocketInterface extends WebSocket { - adminConnection: AdminConnection, - disconnecting: boolean, + adminConnection: AdminConnection; + disconnecting: boolean; } diff --git a/pusher/src/Model/Websocket/ExSocketInterface.ts b/pusher/src/Model/Websocket/ExSocketInterface.ts index 98759142..9a92a0e7 100644 --- a/pusher/src/Model/Websocket/ExSocketInterface.ts +++ b/pusher/src/Model/Websocket/ExSocketInterface.ts @@ -1,23 +1,23 @@ -import {PointInterface} from "./PointInterface"; -import {Identificable} from "./Identificable"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import { PointInterface } from "./PointInterface"; +import { Identificable } from "./Identificable"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; import { BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, - SubMessage + SubMessage, } from "../../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js" -import {CharacterTexture} from "../../Services/AdminApi"; -import {ClientDuplexStream} from "grpc"; -import {Zone} from "_Model/Zone"; +import { WebSocket } from "uWebSockets.js"; +import { CharacterTexture } from "../../Services/AdminApi"; +import { ClientDuplexStream } from "grpc"; +import { Zone } from "_Model/Zone"; export type BackConnection = ClientDuplexStream; export interface CharacterLayer { - name: string, - url: string|undefined + name: string; + url: string | undefined; } export interface ExSocketInterface extends WebSocket, Identificable { @@ -36,12 +36,12 @@ export interface ExSocketInterface extends WebSocket, Identificable { */ emitInBatch: (payload: SubMessage) => void; batchedMessages: BatchMessage; - batchTimeout: NodeJS.Timeout|null; - disconnecting: boolean, - messages: unknown, - tags: string[], - visitCardUrl: string|null, - textures: CharacterTexture[], - backConnection: BackConnection, + batchTimeout: NodeJS.Timeout | null; + disconnecting: boolean; + messages: unknown; + tags: string[]; + visitCardUrl: string | null; + textures: CharacterTexture[]; + backConnection: BackConnection; listenedZones: Set; } diff --git a/pusher/src/Model/Websocket/ItemEventMessage.ts b/pusher/src/Model/Websocket/ItemEventMessage.ts index b1f9203e..1bb7f615 100644 --- a/pusher/src/Model/Websocket/ItemEventMessage.ts +++ b/pusher/src/Model/Websocket/ItemEventMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isItemEventMessageInterface = - new tg.IsInterface().withProperties({ +export const isItemEventMessageInterface = new tg.IsInterface() + .withProperties({ itemId: tg.isNumber, event: tg.isString, state: tg.isUnknown, parameters: tg.isUnknown, - }).get(); + }) + .get(); export type ItemEventMessageInterface = tg.GuardedType; diff --git a/pusher/src/Model/Websocket/Point.ts b/pusher/src/Model/Websocket/Point.ts index c66720ba..19b57d2e 100644 --- a/pusher/src/Model/Websocket/Point.ts +++ b/pusher/src/Model/Websocket/Point.ts @@ -1,6 +1,10 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; -export class Point implements PointInterface{ - constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { - } +export class Point implements PointInterface { + constructor( + public x: number, + public y: number, + public direction: string = "none", + public moving: boolean = false + ) {} } diff --git a/pusher/src/Model/Websocket/PointInterface.ts b/pusher/src/Model/Websocket/PointInterface.ts index afb07a23..d7c7826e 100644 --- a/pusher/src/Model/Websocket/PointInterface.ts +++ b/pusher/src/Model/Websocket/PointInterface.ts @@ -7,11 +7,12 @@ import * as tg from "generic-type-guard"; readonly moving: boolean; }*/ -export const isPointInterface = - new tg.IsInterface().withProperties({ +export const isPointInterface = new tg.IsInterface() + .withProperties({ x: tg.isNumber, y: tg.isNumber, direction: tg.isString, - moving: tg.isBoolean - }).get(); + moving: tg.isBoolean, + }) + .get(); export type PointInterface = tg.GuardedType; diff --git a/pusher/src/Model/Websocket/ProtobufUtils.ts b/pusher/src/Model/Websocket/ProtobufUtils.ts index 89a90acc..bd9cb9c2 100644 --- a/pusher/src/Model/Websocket/ProtobufUtils.ts +++ b/pusher/src/Model/Websocket/ProtobufUtils.ts @@ -1,34 +1,33 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; import { CharacterLayerMessage, ItemEventMessage, PointMessage, - PositionMessage + PositionMessage, } from "../../Messages/generated/messages_pb"; -import {CharacterLayer, ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import { CharacterLayer, ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; import Direction = PositionMessage.Direction; -import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; -import {PositionInterface} from "_Model/PositionInterface"; +import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; +import { PositionInterface } from "_Model/PositionInterface"; export class ProtobufUtils { - public static toPositionMessage(point: PointInterface): PositionMessage { let direction: Direction; switch (point.direction) { - case 'up': + case "up": direction = Direction.UP; break; - case 'down': + case "down": direction = Direction.DOWN; break; - case 'left': + case "left": direction = Direction.LEFT; break; - case 'right': + case "right": direction = Direction.RIGHT; break; default: - throw new Error('unexpected direction'); + throw new Error("unexpected direction"); } const position = new PositionMessage(); @@ -44,16 +43,16 @@ export class ProtobufUtils { let direction: string; switch (position.getDirection()) { case Direction.UP: - direction = 'up'; + direction = "up"; break; case Direction.DOWN: - direction = 'down'; + direction = "down"; break; case Direction.LEFT: - direction = 'left'; + direction = "left"; break; case Direction.RIGHT: - direction = 'right'; + direction = "right"; break; default: throw new Error("Unexpected direction"); @@ -82,7 +81,7 @@ export class ProtobufUtils { event: itemEventMessage.getEvent(), parameters: JSON.parse(itemEventMessage.getParametersjson()), state: JSON.parse(itemEventMessage.getStatejson()), - } + }; } public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { @@ -96,7 +95,7 @@ export class ProtobufUtils { } public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { - return characterLayers.map(function(characterLayer): CharacterLayerMessage { + return characterLayers.map(function (characterLayer): CharacterLayerMessage { const message = new CharacterLayerMessage(); message.setName(characterLayer.name); if (characterLayer.url) { diff --git a/pusher/src/Model/Websocket/ViewportMessage.ts b/pusher/src/Model/Websocket/ViewportMessage.ts index 62e2fc81..ea71ad68 100644 --- a/pusher/src/Model/Websocket/ViewportMessage.ts +++ b/pusher/src/Model/Websocket/ViewportMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isViewport = - new tg.IsInterface().withProperties({ +export const isViewport = new tg.IsInterface() + .withProperties({ left: tg.isNumber, top: tg.isNumber, right: tg.isNumber, bottom: tg.isNumber, - }).get(); + }) + .get(); export type ViewportInterface = tg.GuardedType; diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 318a119b..8eeeb3ef 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -1,16 +1,23 @@ -import {ExSocketInterface} from "./Websocket/ExSocketInterface"; -import {apiClientRepository} from "../Services/ApiClientRepository"; +import { ExSocketInterface } from "./Websocket/ExSocketInterface"; +import { apiClientRepository } from "../Services/ApiClientRepository"; import { BatchToPusherMessage, - CharacterLayerMessage, GroupLeftZoneMessage, GroupUpdateMessage, GroupUpdateZoneMessage, - PointMessage, PositionMessage, UserJoinedMessage, - UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage, + CharacterLayerMessage, + GroupLeftZoneMessage, + GroupUpdateMessage, + GroupUpdateZoneMessage, + PointMessage, + PositionMessage, + UserJoinedMessage, + UserJoinedZoneMessage, + UserLeftZoneMessage, + UserMovedMessage, ZoneMessage, EmoteEventMessage, - CompanionMessage + CompanionMessage, } from "../Messages/generated/messages_pb"; -import {ClientReadableStream} from "grpc"; -import {PositionDispatcher} from "_Model/PositionDispatcher"; +import { ClientReadableStream } from "grpc"; +import { PositionDispatcher } from "_Model/PositionDispatcher"; import Debug from "debug"; const debug = Debug("zone"); @@ -30,24 +37,38 @@ export type MovesCallback = (thing: Movable, position: PositionInterface, listen export type LeavesCallback = (thing: Movable, listener: User) => void;*/ export class UserDescriptor { - private constructor(public readonly userId: number, private name: string, private characterLayers: CharacterLayerMessage[], private position: PositionMessage, private visitCardUrl: string | null, private companion?: CompanionMessage) { + private constructor( + public readonly userId: number, + private name: string, + private characterLayers: CharacterLayerMessage[], + private position: PositionMessage, + private visitCardUrl: string | null, + private companion?: CompanionMessage + ) { if (!Number.isInteger(this.userId)) { - throw new Error('UserDescriptor.userId is not an integer: '+this.userId); + throw new Error("UserDescriptor.userId is not an integer: " + this.userId); } } public static createFromUserJoinedZoneMessage(message: UserJoinedZoneMessage): UserDescriptor { const position = message.getPosition(); if (position === undefined) { - throw new Error('Missing position'); + throw new Error("Missing position"); } - return new UserDescriptor(message.getUserid(), message.getName(), message.getCharacterlayersList(), position, message.getVisitcardurl(), message.getCompanion()); + return new UserDescriptor( + message.getUserid(), + message.getName(), + message.getCharacterlayersList(), + position, + message.getVisitcardurl(), + message.getCompanion() + ); } public update(userMovedMessage: UserMovedMessage) { const position = userMovedMessage.getPosition(); if (position === undefined) { - throw new Error('Missing position'); + throw new Error("Missing position"); } this.position = position; } @@ -78,13 +99,12 @@ export class UserDescriptor { } export class GroupDescriptor { - private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) { - } + private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) {} public static createFromGroupUpdateZoneMessage(message: GroupUpdateZoneMessage): GroupDescriptor { const position = message.getPosition(); if (position === undefined) { - throw new Error('Missing position'); + throw new Error("Missing position"); } return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position); } @@ -97,7 +117,7 @@ export class GroupDescriptor { public toGroupUpdateMessage(): GroupUpdateMessage { const groupUpdateMessage = new GroupUpdateMessage(); if (!Number.isInteger(this.groupId)) { - throw new Error('GroupDescriptor.groupId is not an integer: '+this.groupId); + throw new Error("GroupDescriptor.groupId is not an integer: " + this.groupId); } groupUpdateMessage.setGroupid(this.groupId); groupUpdateMessage.setGroupsize(this.groupSize); @@ -108,8 +128,8 @@ export class GroupDescriptor { } interface ZoneDescriptor { - x: number, - y: number + x: number; + y: number; } export class Zone { @@ -120,21 +140,26 @@ export class Zone { private backConnection!: ClientReadableStream; private isClosing: boolean = false; - constructor(private positionDispatcher: PositionDispatcher, private socketListener: ZoneEventListener, public readonly x: number, public readonly y: number, private onBackFailure: (e: Error|null, zone: Zone) => void) { - } + constructor( + private positionDispatcher: PositionDispatcher, + private socketListener: ZoneEventListener, + public readonly x: number, + public readonly y: number, + private onBackFailure: (e: Error | null, zone: Zone) => void + ) {} /** * Creates a connection to the back server to track the users. */ public async init(): Promise { - debug('Opening connection to zone %d, %d on back server', this.x, this.y); + debug("Opening connection to zone %d, %d on back server", this.x, this.y); const apiClient = await apiClientRepository.getClient(this.positionDispatcher.roomId); const zoneMessage = new ZoneMessage(); zoneMessage.setRoomid(this.positionDispatcher.roomId); zoneMessage.setX(this.x); zoneMessage.setY(this.y); this.backConnection = apiClient.listenZone(zoneMessage); - this.backConnection.on('data', (batch: BatchToPusherMessage) => { + this.backConnection.on("data", (batch: BatchToPusherMessage) => { for (const message of batch.getPayloadList()) { if (message.hasUserjoinedzonemessage()) { const userJoinedZoneMessage = message.getUserjoinedzonemessage() as UserJoinedZoneMessage; @@ -179,33 +204,32 @@ export class Zone { const userDescriptor = this.users.get(userId); if (userDescriptor === undefined) { - console.error('Unexpected move message received for user "'+userId+'"'); + console.error('Unexpected move message received for user "' + userId + '"'); return; } userDescriptor.update(userMovedMessage); this.notifyUserMove(userDescriptor); - } else if(message.hasEmoteeventmessage()) { + } else if (message.hasEmoteeventmessage()) { const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; this.notifyEmote(emoteEventMessage); } else { - throw new Error('Unexpected message'); + throw new Error("Unexpected message"); } - } }); - this.backConnection.on('error', (e) => { + this.backConnection.on("error", (e) => { if (!this.isClosing) { - debug('Error on back connection') + debug("Error on back connection"); this.close(); this.onBackFailure(e, this); } }); - this.backConnection.on('close', () => { + this.backConnection.on("close", () => { if (!this.isClosing) { - debug('Close on back connection') + debug("Close on back connection"); this.close(); this.onBackFailure(null, this); } @@ -213,7 +237,7 @@ export class Zone { } public close(): void { - debug('Closing connection to zone %d, %d on back server', this.x, this.y); + debug("Closing connection to zone %d, %d on back server", this.x, this.y); this.isClosing = true; this.backConnection.cancel(); } @@ -225,7 +249,7 @@ export class Zone { /** * Notify listeners of this zone that this user entered */ - private notifyUserEnter(user: UserDescriptor, oldZone: ZoneDescriptor|undefined) { + private notifyUserEnter(user: UserDescriptor, oldZone: ZoneDescriptor | undefined) { for (const listener of this.listeners) { if (listener.userId === user.userId) { continue; @@ -241,7 +265,7 @@ export class Zone { /** * Notify listeners of this zone that this group entered */ - private notifyGroupEnter(group: GroupDescriptor, oldZone: ZoneDescriptor|undefined) { + private notifyGroupEnter(group: GroupDescriptor, oldZone: ZoneDescriptor | undefined) { for (const listener of this.listeners) { if (oldZone === undefined || !this.isListeningZone(listener, oldZone.x, oldZone.y)) { this.socketListener.onGroupEnters(group, listener); @@ -254,7 +278,7 @@ export class Zone { /** * Notify listeners of this zone that this user left */ - private notifyUserLeft(userId: number, newZone: ZoneDescriptor|undefined) { + private notifyUserLeft(userId: number, newZone: ZoneDescriptor | undefined) { for (const listener of this.listeners) { if (listener.userId === userId) { continue; @@ -279,7 +303,7 @@ export class Zone { /** * Notify listeners of this zone that this group left */ - private notifyGroupLeft(groupId: number, newZone: ZoneDescriptor|undefined) { + private notifyGroupLeft(groupId: number, newZone: ZoneDescriptor | undefined) { for (const listener of this.listeners) { if (listener.groupId === groupId) { continue; diff --git a/pusher/src/Server/server/app.ts b/pusher/src/Server/server/app.ts index 3b98a9b3..4c422d5c 100644 --- a/pusher/src/Server/server/app.ts +++ b/pusher/src/Server/server/app.ts @@ -1,13 +1,13 @@ -import { App as _App, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { App as _App, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class App extends (_App) { - constructor(options: AppOptions = {}) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions = {}) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default App; diff --git a/pusher/src/Server/server/baseapp.ts b/pusher/src/Server/server/baseapp.ts index accd8a99..6d973ac7 100644 --- a/pusher/src/Server/server/baseapp.ts +++ b/pusher/src/Server/server/baseapp.ts @@ -1,116 +1,109 @@ -import { Readable } from 'stream'; -import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { Readable } from "stream"; +import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; -import formData from './formdata'; -import { stob } from './utils'; -import { Handler } from './types'; -import {join} from "path"; +import formData from "./formdata"; +import { stob } from "./utils"; +import { Handler } from "./types"; +import { join } from "path"; -const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; +const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"]; const noOp = () => true; const handleBody = (res: HttpResponse, req: HttpRequest) => { - const contType = req.getHeader('content-type'); + const contType = req.getHeader("content-type"); - res.bodyStream = function() { - const stream = new Readable(); - stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method + res.bodyStream = function () { + const stream = new Readable(); + stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method - this.onData((ab: ArrayBuffer, isLast: boolean) => { - // uint and then slicing is bit faster than slice and then uint - stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any - if (isLast) { - stream.push(null); - } - }); + this.onData((ab: ArrayBuffer, isLast: boolean) => { + // uint and then slicing is bit faster than slice and then uint + stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any + if (isLast) { + stream.push(null); + } + }); - return stream; - }; + return stream; + }; - res.body = () => stob(res.bodyStream()); + res.body = () => stob(res.bodyStream()); - if (contType.includes('application/json')) - res.json = async () => JSON.parse(await res.body()); - if (contTypes.map(t => contType.includes(t)).includes(true)) - res.formData = formData.bind(res, contType); + if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body()); + if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType); }; class BaseApp { - _sockets = new Map(); - ws!: TemplatedApp['ws']; - get!: TemplatedApp['get']; - _post!: TemplatedApp['post']; - _put!: TemplatedApp['put']; - _patch!: TemplatedApp['patch']; - _listen!: TemplatedApp['listen']; + _sockets = new Map(); + ws!: TemplatedApp["ws"]; + get!: TemplatedApp["get"]; + _post!: TemplatedApp["post"]; + _put!: TemplatedApp["put"]; + _patch!: TemplatedApp["patch"]; + _listen!: TemplatedApp["listen"]; - post(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._post(pattern, (res, req) => { - handleBody(res, req); - handler(res, req); - }); - return this; - } + post(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._post(pattern, (res, req) => { + handleBody(res, req); + handler(res, req); + }); + return this; + } - put(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._put(pattern, (res, req) => { - handleBody(res, req); + put(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._put(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - patch(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._patch(pattern, (res, req) => { - handleBody(res, req); + patch(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._patch(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - listen(h: string | number, p: Function | number = noOp, cb?: Function) { - if (typeof p === 'number' && typeof h === 'string') { - this._listen(h, p, socket => { - this._sockets.set(p, socket); - if (cb === undefined) { - throw new Error('cb undefined'); + listen(h: string | number, p: Function | number = noOp, cb?: Function) { + if (typeof p === "number" && typeof h === "string") { + this._listen(h, p, (socket) => { + this._sockets.set(p, socket); + if (cb === undefined) { + throw new Error("cb undefined"); + } + cb(socket); + }); + } else if (typeof h === "number" && typeof p === "function") { + this._listen(h, (socket) => { + this._sockets.set(h, socket); + p(socket); + }); + } else { + throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)"); } - cb(socket); - }); - } else if (typeof h === 'number' && typeof p === 'function') { - this._listen(h, socket => { - this._sockets.set(h, socket); - p(socket); - }); - } else { - throw Error( - 'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)' - ); + + return this; } - return this; - } - - close(port: null | number = null) { - if (port) { - this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); - this._sockets.delete(port); - } else { - this._sockets.forEach(app => { - us_listen_socket_close(app); - }); - this._sockets.clear(); + close(port: null | number = null) { + if (port) { + this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); + this._sockets.delete(port); + } else { + this._sockets.forEach((app) => { + us_listen_socket_close(app); + }); + this._sockets.clear(); + } + return this; } - return this; - } } export default BaseApp; diff --git a/pusher/src/Server/server/formdata.ts b/pusher/src/Server/server/formdata.ts index 9dd08440..66e51db4 100644 --- a/pusher/src/Server/server/formdata.ts +++ b/pusher/src/Server/server/formdata.ts @@ -1,100 +1,99 @@ -import { createWriteStream } from 'fs'; -import { join, dirname } from 'path'; -import Busboy from 'busboy'; -import mkdirp from 'mkdirp'; +import { createWriteStream } from "fs"; +import { join, dirname } from "path"; +import Busboy from "busboy"; +import mkdirp from "mkdirp"; function formData( - contType: string, - options: busboy.BusboyConfig & { - abortOnLimit?: boolean; - tmpDir?: string; - onFile?: ( - fieldname: string, - file: NodeJS.ReadableStream, - filename: string, - encoding: string, - mimetype: string - ) => string; - onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - filename?: (oldName: string) => string; - } = {} + contType: string, + options: busboy.BusboyConfig & { + abortOnLimit?: boolean; + tmpDir?: string; + onFile?: ( + fieldname: string, + file: NodeJS.ReadableStream, + filename: string, + encoding: string, + mimetype: string + ) => string; + onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + filename?: (oldName: string) => string; + } = {} ) { - console.log('Enter form data'); - options.headers = { - 'content-type': contType - }; + console.log("Enter form data"); + options.headers = { + "content-type": contType, + }; - return new Promise((resolve, reject) => { - const busb = new Busboy(options); - const ret = {}; + return new Promise((resolve, reject) => { + const busb = new Busboy(options); + const ret = {}; - this.bodyStream().pipe(busb); + this.bodyStream().pipe(busb); - busb.on('limit', () => { - if (options.abortOnLimit) { - reject(Error('limit')); - } + busb.on("limit", () => { + if (options.abortOnLimit) { + reject(Error("limit")); + } + }); + + busb.on("file", function (fieldname, file, filename, encoding, mimetype) { + const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = { + filename, + encoding, + mimetype, + filePath: undefined, + }; + + if (typeof options.tmpDir === "string") { + if (typeof options.filename === "function") filename = options.filename(filename); + const fileToSave = join(options.tmpDir, filename); + mkdirp(dirname(fileToSave)); + + file.pipe(createWriteStream(fileToSave)); + value.filePath = fileToSave; + } + if (typeof options.onFile === "function") { + value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; + } + + setRetValue(ret, fieldname, value); + }); + + busb.on("field", function (fieldname, value) { + if (typeof options.onField === "function") options.onField(fieldname, value); + + setRetValue(ret, fieldname, value); + }); + + busb.on("finish", function () { + resolve(ret); + }); + + busb.on("error", reject); }); - - busb.on('file', function(fieldname, file, filename, encoding, mimetype) { - const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = { - filename, - encoding, - mimetype, - filePath: undefined - }; - - if (typeof options.tmpDir === 'string') { - if (typeof options.filename === 'function') filename = options.filename(filename); - const fileToSave = join(options.tmpDir, filename); - mkdirp(dirname(fileToSave)); - - file.pipe(createWriteStream(fileToSave)); - value.filePath = fileToSave; - } - if (typeof options.onFile === 'function') { - value.filePath = - options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; - } - - setRetValue(ret, fieldname, value); - }); - - busb.on('field', function(fieldname, value) { - if (typeof options.onField === 'function') options.onField(fieldname, value); - - setRetValue(ret, fieldname, value); - }); - - busb.on('finish', function() { - resolve(ret); - }); - - busb.on('error', reject); - }); } function setRetValue( - ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any - fieldname: string, - value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any + ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + fieldname: string, + value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any ) { - if (fieldname.endsWith('[]')) { - fieldname = fieldname.slice(0, fieldname.length - 2); - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); + if (fieldname.endsWith("[]")) { + fieldname = fieldname.slice(0, fieldname.length - 2); + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else { + ret[fieldname] = [value]; + } } else { - ret[fieldname] = [value]; + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else if (ret[fieldname]) { + ret[fieldname] = [ret[fieldname], value]; + } else { + ret[fieldname] = value; + } } - } else { - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); - } else if (ret[fieldname]) { - ret[fieldname] = [ret[fieldname], value]; - } else { - ret[fieldname] = value; - } - } } export default formData; diff --git a/pusher/src/Server/server/sslapp.ts b/pusher/src/Server/server/sslapp.ts index 46ae89a5..80df0e4a 100644 --- a/pusher/src/Server/server/sslapp.ts +++ b/pusher/src/Server/server/sslapp.ts @@ -1,13 +1,13 @@ -import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class SSLApp extends (_SSLApp) { - constructor(options: AppOptions) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default SSLApp; diff --git a/pusher/src/Server/server/types.ts b/pusher/src/Server/server/types.ts index 3d0f48c7..afc21d17 100644 --- a/pusher/src/Server/server/types.ts +++ b/pusher/src/Server/server/types.ts @@ -1,9 +1,9 @@ -import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; export type UwsApp = { - (options: AppOptions): TemplatedApp; - new (options: AppOptions): TemplatedApp; - prototype: TemplatedApp; + (options: AppOptions): TemplatedApp; + new (options: AppOptions): TemplatedApp; + prototype: TemplatedApp; }; export type Handler = (res: HttpResponse, req: HttpRequest) => void; diff --git a/pusher/src/Server/server/utils.ts b/pusher/src/Server/server/utils.ts index 80ea3938..9816de54 100644 --- a/pusher/src/Server/server/utils.ts +++ b/pusher/src/Server/server/utils.ts @@ -1,37 +1,36 @@ -import { ReadStream } from 'fs'; +import { ReadStream } from "fs"; -function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any - const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( - Object.keys(from) - ); - ownProps.forEach(prop => { - if (prop === 'constructor' || from[prop] === undefined) return; - if (who[prop] && overwrite) { - who[`_${prop}`] = who[prop]; - } - if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); - else who[prop] = from[prop]; - }); +function extend(who: any, from: any, overwrite = true) { + // eslint-disable-line @typescript-eslint/no-explicit-any + const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); + ownProps.forEach((prop) => { + if (prop === "constructor" || from[prop] === undefined) return; + if (who[prop] && overwrite) { + who[`_${prop}`] = who[prop]; + } + if (typeof from[prop] === "function") who[prop] = from[prop].bind(who); + else who[prop] = from[prop]; + }); } function stob(stream: ReadStream): Promise { - return new Promise(resolve => { - const buffers: Buffer[] = []; - stream.on('data', buffers.push.bind(buffers)); + return new Promise((resolve) => { + const buffers: Buffer[] = []; + stream.on("data", buffers.push.bind(buffers)); - stream.on('end', () => { - switch (buffers.length) { - case 0: - resolve(Buffer.allocUnsafe(0)); - break; - case 1: - resolve(buffers[0]); - break; - default: - resolve(Buffer.concat(buffers)); - } + stream.on("end", () => { + switch (buffers.length) { + case 0: + resolve(Buffer.allocUnsafe(0)); + break; + case 1: + resolve(buffers[0]); + break; + default: + resolve(Buffer.concat(buffers)); + } + }); }); - }); } export { extend, stob }; diff --git a/pusher/src/Server/sifrr.server.ts b/pusher/src/Server/sifrr.server.ts index 47fba02c..4ef03721 100644 --- a/pusher/src/Server/sifrr.server.ts +++ b/pusher/src/Server/sifrr.server.ts @@ -1,19 +1,19 @@ -import { parse } from 'query-string'; -import { HttpRequest } from 'uWebSockets.js'; -import App from './server/app'; -import SSLApp from './server/sslapp'; -import * as types from './server/types'; +import { parse } from "query-string"; +import { HttpRequest } from "uWebSockets.js"; +import App from "./server/app"; +import SSLApp from "./server/sslapp"; +import * as types from "./server/types"; const getQuery = (req: HttpRequest) => { - return parse(req.getQuery()); + return parse(req.getQuery()); }; export { App, SSLApp, getQuery }; -export * from './server/types'; +export * from "./server/types"; export default { - App, - SSLApp, - getQuery, - ...types + App, + SSLApp, + getQuery, + ...types, }; diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index fbd7a070..2cbac52c 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -1,123 +1,147 @@ -import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; +import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable"; import Axios from "axios"; -import {GameRoomPolicyTypes} from "_Model/PusherRoom"; +import { GameRoomPolicyTypes } from "_Model/PusherRoom"; export interface AdminApiData { - organizationSlug: string - worldSlug: string - roomSlug: string - mapUrlStart: string - tags: string[] - policy_type: number - userUuid: string - messages?: unknown[], - textures: CharacterTexture[] + organizationSlug: string; + worldSlug: string; + roomSlug: string; + mapUrlStart: string; + tags: string[]; + policy_type: number; + userUuid: string; + messages?: unknown[]; + textures: CharacterTexture[]; } export interface MapDetailsData { - roomSlug: string, - mapUrl: string, - policy_type: GameRoomPolicyTypes, - tags: string[], + roomSlug: string; + mapUrl: string; + policy_type: GameRoomPolicyTypes; + tags: string[]; } export interface AdminBannedData { - is_banned: boolean, - message: string + is_banned: boolean; + message: string; } export interface CharacterTexture { - id: number, - level: number, - url: string, - rights: string + id: number; + level: number; + url: string; + rights: string; } export interface FetchMemberDataByUuidResponse { uuid: string; tags: string[]; - visitCardUrl: string|null; + visitCardUrl: string | null; textures: CharacterTexture[]; messages: unknown[]; anonymous?: boolean; } class AdminApi { - - async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { + async fetchMapDetails( + organizationSlug: string, + worldSlug: string, + roomSlug: string | undefined + ): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } - const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { + const params: { organizationSlug: string; worldSlug: string; roomSlug?: string } = { organizationSlug, - worldSlug + worldSlug, }; if (roomSlug) { params.roomSlug = roomSlug; } - const res = await Axios.get(ADMIN_API_URL + '/api/map', - { - headers: {"Authorization": `${ADMIN_API_TOKEN}`}, - params - } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/map", { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + params, + }); return res.data; } async fetchMemberDataByUuid(uuid: string, roomId: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } - const res = await Axios.get(ADMIN_API_URL+'/api/room/access', - { params: {uuid, roomId}, headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/room/access", { + params: { uuid, roomId }, + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } async fetchMemberDataByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } async fetchCheckUserByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/check-user/" + organizationMemberToken, { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } - reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) { - return Axios.post(`${ADMIN_API_URL}/api/report`, { + reportPlayer( + reportedUserUuid: string, + reportedUserComment: string, + reporterUserUuid: string, + reportWorldSlug: string + ) { + return Axios.post( + `${ADMIN_API_URL}/api/report`, + { reportedUserUuid, reportedUserComment, reporterUserUuid, - reportWorldSlug + reportWorldSlug, }, { - headers: {"Authorization": `${ADMIN_API_TOKEN}`} - }); + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + } + ); } - async verifyBanUser(organizationMemberToken: string, ipAddress: string, organization: string, world: string): Promise { + async verifyBanUser( + organizationMemberToken: string, + ipAddress: string, + organization: string, + world: string + ): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - return Axios.get(ADMIN_API_URL + '/api/check-moderate-user/'+organization+'/'+world+'?ipAddress='+ipAddress+'&token='+organizationMemberToken, - {headers: {"Authorization": `${ADMIN_API_TOKEN}`}} + return Axios.get( + ADMIN_API_URL + + "/api/check-moderate-user/" + + organization + + "/" + + world + + "?ipAddress=" + + ipAddress + + "&token=" + + organizationMemberToken, + { headers: { Authorization: `${ADMIN_API_TOKEN}` } } ).then((data) => { return data.data; }); diff --git a/pusher/src/Services/ApiClientRepository.ts b/pusher/src/Services/ApiClientRepository.ts index c1c6bd38..59e181ae 100644 --- a/pusher/src/Services/ApiClientRepository.ts +++ b/pusher/src/Services/ApiClientRepository.ts @@ -1,14 +1,14 @@ /** * A class to get connections to the correct "api" server given a room name. */ -import {RoomManagerClient} from "../Messages/generated/messages_grpc_pb"; -import grpc from 'grpc'; -import crypto from 'crypto'; -import {API_URL} from "../Enum/EnvironmentVariable"; +import { RoomManagerClient } from "../Messages/generated/messages_grpc_pb"; +import grpc from "grpc"; +import crypto from "crypto"; +import { API_URL } from "../Enum/EnvironmentVariable"; import Debug from "debug"; -const debug = Debug('apiClientRespository'); +const debug = Debug("apiClientRespository"); class ApiClientRepository { private roomManagerClients: RoomManagerClient[] = []; @@ -16,23 +16,26 @@ class ApiClientRepository { public constructor(private apiUrls: string[]) {} public async getClient(roomId: string): Promise { - const array = new Uint32Array(crypto.createHash('md5').update(roomId).digest()); + const array = new Uint32Array(crypto.createHash("md5").update(roomId).digest()); const index = array[0] % this.apiUrls.length; let client = this.roomManagerClients[index]; if (client === undefined) { - this.roomManagerClients[index] = client = new RoomManagerClient(this.apiUrls[index], grpc.credentials.createInsecure()); - debug('Mapping room %s to API server %s', roomId, this.apiUrls[index]) + this.roomManagerClients[index] = client = new RoomManagerClient( + this.apiUrls[index], + grpc.credentials.createInsecure() + ); + debug("Mapping room %s to API server %s", roomId, this.apiUrls[index]); } return Promise.resolve(client); } public async getAllClients(): Promise { - return [await this.getClient('')]; + return [await this.getClient("")]; } } -const apiClientRepository = new ApiClientRepository(API_URL.split(',')); +const apiClientRepository = new ApiClientRepository(API_URL.split(",")); export { apiClientRepository }; diff --git a/pusher/src/Services/ArrayHelper.ts b/pusher/src/Services/ArrayHelper.ts index 67321d1b..8af1da9f 100644 --- a/pusher/src/Services/ArrayHelper.ts +++ b/pusher/src/Services/ArrayHelper.ts @@ -1,3 +1,3 @@ -export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { - return array1.filter(value => array2.includes(value)).length > 0; -} \ No newline at end of file +export const arrayIntersect = (array1: string[], array2: string[]): boolean => { + return array1.filter((value) => array2.includes(value)).length > 0; +}; diff --git a/pusher/src/Services/ClientEventsEmitter.ts b/pusher/src/Services/ClientEventsEmitter.ts index 7b888ef6..0f56d55c 100644 --- a/pusher/src/Services/ClientEventsEmitter.ts +++ b/pusher/src/Services/ClientEventsEmitter.ts @@ -1,7 +1,7 @@ -const EventEmitter = require('events'); +const EventEmitter = require("events"); -const clientJoinEvent = 'clientJoin'; -const clientLeaveEvent = 'clientLeave'; +const clientJoinEvent = "clientJoin"; +const clientLeaveEvent = "clientLeave"; class ClientEventsEmitter extends EventEmitter { emitClientJoin(clientUUid: string, roomId: string): void { @@ -11,7 +11,7 @@ class ClientEventsEmitter extends EventEmitter { emitClientLeave(clientUUid: string, roomId: string): void { this.emit(clientLeaveEvent, clientUUid, roomId); } - + registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void { this.on(clientJoinEvent, callback); } @@ -29,4 +29,4 @@ class ClientEventsEmitter extends EventEmitter { } } -export const clientEventsEmitter = new ClientEventsEmitter(); \ No newline at end of file +export const clientEventsEmitter = new ClientEventsEmitter(); diff --git a/pusher/src/Services/CpuTracker.ts b/pusher/src/Services/CpuTracker.ts index c7d57f3d..3d06ca70 100644 --- a/pusher/src/Services/CpuTracker.ts +++ b/pusher/src/Services/CpuTracker.ts @@ -1,6 +1,6 @@ -import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; +import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable"; -function secNSec2ms(secNSec: Array|number) { +function secNSec2ms(secNSec: Array | number) { if (Array.isArray(secNSec)) { return secNSec[0] * 1000 + secNSec[1] / 1000000; } @@ -12,17 +12,17 @@ class CpuTracker { private overHeating: boolean = false; constructor() { - let time = process.hrtime.bigint() - let usage = process.cpuUsage() + let time = process.hrtime.bigint(); + let usage = process.cpuUsage(); setInterval(() => { const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage) - usage = process.cpuUsage() + const elapUsage = process.cpuUsage(usage); + usage = process.cpuUsage(); const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user) - const elapSystMS = secNSec2ms(elapUsage.system) - this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) + const elapUserMS = secNSec2ms(elapUsage.user); + const elapSystMS = secNSec2ms(elapUsage.system); + this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000); time = elapTime; diff --git a/pusher/src/Services/GaugeManager.ts b/pusher/src/Services/GaugeManager.ts index f8af822b..9780d89d 100644 --- a/pusher/src/Services/GaugeManager.ts +++ b/pusher/src/Services/GaugeManager.ts @@ -1,4 +1,4 @@ -import {Counter, Gauge} from "prom-client"; +import { Counter, Gauge } from "prom-client"; //this class should manage all the custom metrics used by prometheus class GaugeManager { @@ -9,25 +9,25 @@ class GaugeManager { constructor() { this.nbClientsGauge = new Gauge({ - name: 'workadventure_nb_sockets', - help: 'Number of connected sockets', - labelNames: [ ] + name: "workadventure_nb_sockets", + help: "Number of connected sockets", + labelNames: [], }); this.nbClientsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_clients_per_room', - help: 'Number of clients per room', - labelNames: [ 'room' ] + name: "workadventure_nb_clients_per_room", + help: "Number of clients per room", + labelNames: ["room"], }); this.nbGroupsPerRoomCounter = new Counter({ - name: 'workadventure_counter_groups_per_room', - help: 'Counter of groups per room', - labelNames: [ 'room' ] + name: "workadventure_counter_groups_per_room", + help: "Counter of groups per room", + labelNames: ["room"], }); this.nbGroupsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_groups_per_room', - help: 'Number of groups per room', - labelNames: [ 'room' ] + name: "workadventure_nb_groups_per_room", + help: "Number of groups per room", + labelNames: ["room"], }); } @@ -42,13 +42,13 @@ class GaugeManager { } incNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomCounter.inc({ room: roomId }) - this.nbGroupsPerRoomGauge.inc({ room: roomId }) + this.nbGroupsPerRoomCounter.inc({ room: roomId }); + this.nbGroupsPerRoomGauge.inc({ room: roomId }); } - + decNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomGauge.dec({ room: roomId }) + this.nbGroupsPerRoomGauge.dec({ room: roomId }); } } -export const gaugeManager = new GaugeManager(); \ No newline at end of file +export const gaugeManager = new GaugeManager(); diff --git a/pusher/src/Services/IoSocketHelpers.ts b/pusher/src/Services/IoSocketHelpers.ts index e90e0874..2da7c430 100644 --- a/pusher/src/Services/IoSocketHelpers.ts +++ b/pusher/src/Services/IoSocketHelpers.ts @@ -1,6 +1,6 @@ -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; -import {BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; +import { BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage } from "../Messages/generated/messages_pb"; +import { WebSocket } from "uWebSockets.js"; export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -33,4 +33,3 @@ export function emitError(Client: WebSocket, message: string): void { } console.warn(message); } - diff --git a/pusher/src/Services/JWTTokenManager.ts b/pusher/src/Services/JWTTokenManager.ts index 68d5488a..2e82929e 100644 --- a/pusher/src/Services/JWTTokenManager.ts +++ b/pusher/src/Services/JWTTokenManager.ts @@ -1,73 +1,77 @@ -import {ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; -import {uuid} from "uuidv4"; +import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable"; +import { uuid } from "uuidv4"; import Jwt from "jsonwebtoken"; -import {TokenInterface} from "../Controller/AuthenticateController"; -import {adminApi, AdminBannedData} from "../Services/AdminApi"; +import { TokenInterface } from "../Controller/AuthenticateController"; +import { adminApi, AdminBannedData } from "../Services/AdminApi"; class JWTTokenManager { - public createJWTToken(userUuid: string) { - return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token + return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token } public async getUserUuidFromToken(token: unknown, ipAddress?: string, room?: string): Promise { - if (!token) { - throw new Error('An authentication error happened, a user tried to connect without a token.'); + throw new Error("An authentication error happened, a user tried to connect without a token."); } - if (typeof(token) !== "string") { - throw new Error('Token is expected to be a string'); + if (typeof token !== "string") { + throw new Error("Token is expected to be a string"); } - - if(token === 'test') { + if (token === "test") { if (ALLOW_ARTILLERY) { return uuid(); } else { - throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); + throw new Error( + "In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'" + ); } } return new Promise((resolve, reject) => { - Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { + Jwt.verify(token, SECRET_KEY, {}, (err, tokenDecoded) => { const tokenInterface = tokenDecoded as TokenInterface; if (err) { - console.error('An authentication error happened, invalid JsonWebToken.', err); - reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message)); + console.error("An authentication error happened, invalid JsonWebToken.", err); + reject(new Error("An authentication error happened, invalid JsonWebToken. " + err.message)); return; } if (tokenDecoded === undefined) { - console.error('Empty token found.'); - reject(new Error('Empty token found.')); + console.error("Empty token found."); + reject(new Error("Empty token found.")); return; } //verify token if (!this.isValidToken(tokenInterface)) { - reject(new Error('Authentication error, invalid token structure.')); + reject(new Error("Authentication error, invalid token structure.")); return; } if (ADMIN_API_URL) { //verify user in admin let promise = new Promise((resolve) => resolve()); - if(ipAddress && room) { + if (ipAddress && room) { promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, room); } - promise.then(() => { - adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => { - resolve(tokenInterface.userUuid); - }).catch((err) => { - //anonymous user - if (err.response && err.response.status && err.response.status === 404) { - resolve(tokenInterface.userUuid); - return; - } + promise + .then(() => { + adminApi + .fetchCheckUserByToken(tokenInterface.userUuid) + .then(() => { + resolve(tokenInterface.userUuid); + }) + .catch((err) => { + //anonymous user + if (err.response && err.response.status && err.response.status === 404) { + resolve(tokenInterface.userUuid); + return; + } + reject(err); + }); + }) + .catch((err) => { reject(err); }); - }).catch((err) => { - reject(err); - }); } else { resolve(tokenInterface.userUuid); } @@ -76,30 +80,32 @@ class JWTTokenManager { } private verifyBanUser(userUuid: string, ipAddress: string, room: string): Promise { - const parts = room.split('/'); - if (parts.length < 3 || parts[0] !== '@') { + const parts = room.split("/"); + if (parts.length < 3 || parts[0] !== "@") { return Promise.resolve({ is_banned: false, - message: '' + message: "", }); } const organization = parts[1]; const world = parts[2]; - return adminApi.verifyBanUser(userUuid, ipAddress, organization, world).then((data: AdminBannedData) => { - if (data && data.is_banned) { - throw new Error('User was banned'); - } - return data; - }).catch((err) => { - throw err; - }); + return adminApi + .verifyBanUser(userUuid, ipAddress, organization, world) + .then((data: AdminBannedData) => { + if (data && data.is_banned) { + throw new Error("User was banned"); + } + return data; + }) + .catch((err) => { + throw err; + }); } private isValidToken(token: object): token is TokenInterface { - return !(typeof((token as TokenInterface).userUuid) !== 'string'); + return !(typeof (token as TokenInterface).userUuid !== "string"); } - } export const jwtTokenManager = new JWTTokenManager(); diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 319d2b5e..8a0d3673 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -1,5 +1,5 @@ -import {PusherRoom} from "../Model/PusherRoom"; -import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; +import { PusherRoom } from "../Model/PusherRoom"; +import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; import { GroupDeleteMessage, ItemEventMessage, @@ -31,21 +31,21 @@ import { RefreshRoomMessage, EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; -import {adminApi, CharacterTexture} from "./AdminApi"; -import {emitInBatch} from "./IoSocketHelpers"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; +import { adminApi, CharacterTexture } from "./AdminApi"; +import { emitInBatch } from "./IoSocketHelpers"; import Jwt from "jsonwebtoken"; -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {clientEventsEmitter} from "./ClientEventsEmitter"; -import {gaugeManager} from "./GaugeManager"; -import {apiClientRepository} from "./ApiClientRepository"; -import {GroupDescriptor, UserDescriptor, ZoneEventListener} from "_Model/Zone"; +import { JITSI_URL } from "../Enum/EnvironmentVariable"; +import { clientEventsEmitter } from "./ClientEventsEmitter"; +import { gaugeManager } from "./GaugeManager"; +import { apiClientRepository } from "./ApiClientRepository"; +import { GroupDescriptor, UserDescriptor, ZoneEventListener } from "_Model/Zone"; import Debug from "debug"; -import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; -import {WebSocket} from "uWebSockets.js"; +import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; +import { WebSocket } from "uWebSockets.js"; -const debug = Debug('socket'); +const debug = Debug("socket"); interface AdminSocketRoomsList { [index: string]: number; @@ -55,12 +55,11 @@ interface AdminSocketUsersList { } export interface AdminSocketData { - rooms: AdminSocketRoomsList, - users: AdminSocketUsersList, + rooms: AdminSocketRoomsList; + users: AdminSocketUsersList; } export class SocketManager implements ZoneEventListener { - private rooms: Map = new Map(); private sockets: Map = new Map(); @@ -78,47 +77,53 @@ export class SocketManager implements ZoneEventListener { const adminRoomStream = apiClient.adminRoom(); client.adminConnection = adminRoomStream; - adminRoomStream.on('data', (message: ServerToAdminClientMessage) => { - - if (message.hasUserjoinedroom()) { - const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; - if (!client.disconnecting) { - client.send(JSON.stringify({ - type: 'MemberJoin', - data: { - uuid: userJoinedRoomMessage.getUuid(), - name: userJoinedRoomMessage.getName(), - ipAddress: userJoinedRoomMessage.getIpaddress(), - roomId: roomId, - } - })); + adminRoomStream + .on("data", (message: ServerToAdminClientMessage) => { + if (message.hasUserjoinedroom()) { + const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; + if (!client.disconnecting) { + client.send( + JSON.stringify({ + type: "MemberJoin", + data: { + uuid: userJoinedRoomMessage.getUuid(), + name: userJoinedRoomMessage.getName(), + ipAddress: userJoinedRoomMessage.getIpaddress(), + roomId: roomId, + }, + }) + ); + } + } else if (message.hasUserleftroom()) { + const userLeftRoomMessage = message.getUserleftroom() as UserLeftRoomMessage; + if (!client.disconnecting) { + client.send( + JSON.stringify({ + type: "MemberLeave", + data: { + uuid: userLeftRoomMessage.getUuid(), + }, + }) + ); + } + } else { + throw new Error("Unexpected admin message"); } - } else if (message.hasUserleftroom()) { - const userLeftRoomMessage = message.getUserleftroom() as UserLeftRoomMessage; + }) + .on("end", () => { + console.warn("Admin connection lost to back server"); + // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. if (!client.disconnecting) { - client.send(JSON.stringify({ - type: 'MemberLeave', - data: { - uuid: userLeftRoomMessage.getUuid() - } - })); + this.closeWebsocketConnection(client, 1011, "Connection lost to back server"); } - } else { - throw new Error('Unexpected admin message'); - } - }).on('end', () => { - console.warn('Admin connection lost to back server'); - // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Connection lost to back server'); - } - console.log('A user left'); - }).on('error', (err: Error) => { - console.error('Error in connection to back server:', err); - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Error while connecting to back server'); - } - }); + console.log("A user left"); + }) + .on("error", (err: Error) => { + console.error("Error in connection to back server:", err); + if (!client.disconnecting) { + this.closeWebsocketConnection(client, 1011, "Error while connecting to back server"); + } + }); const message = new AdminPusherToBackMessage(); message.setSubscribetoroom(roomId); @@ -126,14 +131,14 @@ export class SocketManager implements ZoneEventListener { adminRoomStream.write(message); } - leaveAdminRoom(socket : ExAdminSocketInterface) { + leaveAdminRoom(socket: ExAdminSocketInterface) { if (socket.adminConnection) { socket.adminConnection.end(); } } - getAdminSocketDataFor(roomId:string): AdminSocketData { - throw new Error('Not reimplemented yet'); + getAdminSocketDataFor(roomId: string): AdminSocketData { + throw new Error("Not reimplemented yet"); /*const data:AdminSocketData = { rooms: {}, users: {}, @@ -153,7 +158,6 @@ export class SocketManager implements ZoneEventListener { async handleJoinRoom(client: ExSocketInterface): Promise { const viewport = client.viewport; try { - const joinRoomMessage = new JoinRoomMessage(); joinRoomMessage.setUseruuid(client.userUuid); joinRoomMessage.setIpaddress(client.IPAddress); @@ -176,46 +180,49 @@ export class SocketManager implements ZoneEventListener { joinRoomMessage.addCharacterlayer(characterLayerMessage); } - - console.log('Calling joinRoom') + console.log("Calling joinRoom"); const apiClient = await apiClientRepository.getClient(client.roomId); const streamToPusher = apiClient.joinRoom(); clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); client.backConnection = streamToPusher; - streamToPusher.on('data', (message: ServerToClientMessage) => { - if (message.hasRoomjoinedmessage()) { - client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); - // TODO: do we need this.sockets anymore? - this.sockets.set(client.userId, client); + streamToPusher + .on("data", (message: ServerToClientMessage) => { + if (message.hasRoomjoinedmessage()) { + client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); + // TODO: do we need this.sockets anymore? + this.sockets.set(client.userId, client); - // If this is the first message sent, send back the viewport. - this.handleViewport(client, viewport); - } - - if (message.hasRefreshroommessage()) { - const refreshMessage:RefreshRoomMessage = message.getRefreshroommessage() as unknown as RefreshRoomMessage; - this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber()) - } + // If this is the first message sent, send back the viewport. + this.handleViewport(client, viewport); + } - // Let's pass data over from the back to the client. - if (!client.disconnecting) { - client.send(message.serializeBinary().buffer, true); - } - }).on('end', () => { - console.warn('Connection lost to back server'); - // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Connection lost to back server'); - } - console.log('A user left'); - }).on('error', (err: Error) => { - console.error('Error in connection to back server:', err); - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Error while connecting to back server'); - } - }); + if (message.hasRefreshroommessage()) { + const refreshMessage: RefreshRoomMessage = + message.getRefreshroommessage() as unknown as RefreshRoomMessage; + this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber()); + } + + // Let's pass data over from the back to the client. + if (!client.disconnecting) { + client.send(message.serializeBinary().buffer, true); + } + }) + .on("end", () => { + console.warn("Connection lost to back server"); + // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. + if (!client.disconnecting) { + this.closeWebsocketConnection(client, 1011, "Connection lost to back server"); + } + console.log("A user left"); + }) + .on("error", (err: Error) => { + console.error("Error in connection to back server:", err); + if (!client.disconnecting) { + this.closeWebsocketConnection(client, 1011, "Error while connecting to back server"); + } + }); const pusherToBackMessage = new PusherToBackMessage(); pusherToBackMessage.setJoinroommessage(joinRoomMessage); @@ -226,7 +233,7 @@ export class SocketManager implements ZoneEventListener { } } - private closeWebsocketConnection(client: ExSocketInterface|ExAdminSocketInterface, code: number, reason: string) { + private closeWebsocketConnection(client: ExSocketInterface | ExAdminSocketInterface, code: number, reason: string) { client.disconnecting = true; //this.leaveRoom(client); //client.close(); @@ -257,15 +264,13 @@ export class SocketManager implements ZoneEventListener { const viewport = userMovesMessage.getViewport(); if (viewport === undefined) { - throw new Error('Missing viewport in UserMovesMessage'); + throw new Error("Missing viewport in UserMovesMessage"); } // Now, we need to listen to the correct viewport. - this.handleViewport(client, viewport.toObject()) + this.handleViewport(client, viewport.toObject()); } - - onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void { const subMessage = new SubMessage(); subMessage.setEmoteeventmessage(emoteMessage); @@ -299,11 +304,16 @@ export class SocketManager implements ZoneEventListener { try { const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); if (!reportedSocket) { - throw 'reported socket user not found'; + throw "reported socket user not found"; } //TODO report user on admin application - //todo: move to back because this fail if the reported player is in another pusher. - await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2]) + //todo: move to back because this fail if the reported player is in another pusher. + await adminApi.reportPlayer( + reportedSocket.userUuid, + reportPlayerMessage.getReportcomment(), + client.userUuid, + client.roomId.split("/")[2] + ); } catch (e) { console.error('An error occurred on "handleReportMessage"'); console.error(e); @@ -325,14 +335,14 @@ export class SocketManager implements ZoneEventListener { } private searchClientByIdOrFail(userId: number): ExSocketInterface { - const client: ExSocketInterface|undefined = this.sockets.get(userId); + const client: ExSocketInterface | undefined = this.sockets.get(userId); if (client === undefined) { throw new Error("Could not find user with id " + userId); } return client; } - leaveRoom(socket : ExSocketInterface) { + leaveRoom(socket: ExSocketInterface) { // leave previous room and world try { if (socket.roomId) { @@ -340,15 +350,15 @@ export class SocketManager implements ZoneEventListener { //user leaves room const room: PusherRoom | undefined = this.rooms.get(socket.roomId); if (room) { - debug('Leaving room %s.', socket.roomId); - + debug("Leaving room %s.", socket.roomId); + room.leave(socket); if (room.isEmpty()) { this.rooms.delete(socket.roomId); - debug('Room %s is empty. Deleting.', socket.roomId); + debug("Room %s is empty. Deleting.", socket.roomId); } } else { - console.error('Could not find the GameRoom the user is leaving!'); + console.error("Could not find the GameRoom the user is leaving!"); } //user leave previous room //Client.leave(Client.roomId); @@ -356,7 +366,7 @@ export class SocketManager implements ZoneEventListener { //delete Client.roomId; this.sockets.delete(socket.userId); clientEventsEmitter.emitClientLeave(socket.userUuid, socket.roomId); - console.log('A user left (', this.sockets.size, ' connected users)'); + console.log("A user left (", this.sockets.size, " connected users)"); } } } finally { @@ -368,27 +378,27 @@ export class SocketManager implements ZoneEventListener { async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room - let world = this.rooms.get(roomId) - if(world === undefined){ + let world = this.rooms.get(roomId); + if (world === undefined) { world = new PusherRoom(roomId, this); if (!world.public) { await this.updateRoomWithAdminData(world); } this.rooms.set(roomId, world); } - return Promise.resolve(world) + return Promise.resolve(world); } public async updateRoomWithAdminData(world: PusherRoom): Promise { - const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) + const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug); world.tags = data.tags; world.policyType = Number(data.policy_type); } emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) { - if (!client.tags.includes('admin')) { + if (!client.tags.includes("admin")) { //In case of xss injection, we just kill the connection. - throw 'Client is not an admin!'; + throw "Client is not an admin!"; } const pusherToBackMessage = new PusherToBackMessage(); pusherToBackMessage.setPlayglobalmessage(playglobalmessage); @@ -399,44 +409,48 @@ export class SocketManager implements ZoneEventListener { public getWorlds(): Map { return this.rooms; } - + searchClientByUuid(uuid: string): ExSocketInterface | null { - for(const socket of this.sockets.values()){ - if(socket.userUuid === uuid){ + for (const socket of this.sockets.values()) { + if (socket.userUuid === uuid) { return socket; } } return null; } - public handleQueryJitsiJwtMessage(client: ExSocketInterface, queryJitsiJwtMessage: QueryJitsiJwtMessage) { try { const room = queryJitsiJwtMessage.getJitsiroom(); const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead. - if (SECRET_JITSI_KEY === '') { - throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); + if (SECRET_JITSI_KEY === "") { + throw new Error( + "You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi." + ); } // Let's see if the current client has const isAdmin = client.tags.includes(tag); - const jwt = Jwt.sign({ - "aud": "jitsi", - "iss": JITSI_ISS, - "sub": JITSI_URL, - "room": room, - "moderator": isAdmin - }, SECRET_JITSI_KEY, { - expiresIn: '1d', - algorithm: "HS256", - header: - { - "alg": "HS256", - "typ": "JWT" - } - }); + const jwt = Jwt.sign( + { + aud: "jitsi", + iss: JITSI_ISS, + sub: JITSI_URL, + room: room, + moderator: isAdmin, + }, + SECRET_JITSI_KEY, + { + expiresIn: "1d", + algorithm: "HS256", + header: { + alg: "HS256", + typ: "JWT", + }, + } + ); const sendJitsiJwtMessage = new SendJitsiJwtMessage(); sendJitsiJwtMessage.setJitsiroom(room); @@ -447,7 +461,7 @@ export class SocketManager implements ZoneEventListener { client.send(serverToClientMessage.serializeBinary().buffer, true); } catch (e) { - console.error('An error occured while generating the Jitsi JWT token: ', e); + console.error("An error occured while generating the Jitsi JWT token: ", e); } } @@ -471,7 +485,7 @@ export class SocketManager implements ZoneEventListener { backAdminMessage.setType(type); backConnection.sendAdminMessage(backAdminMessage, (error) => { if (error !== null) { - console.error('Error while sending admin message', error); + console.error("Error while sending admin message", error); } }); } @@ -496,7 +510,7 @@ export class SocketManager implements ZoneEventListener { banMessage.setType(type); backConnection.ban(banMessage, (error) => { if (error !== null) { - console.error('Error while sending admin message', error); + console.error("Error while sending admin message", error); } }); } @@ -504,25 +518,28 @@ export class SocketManager implements ZoneEventListener { /** * Merges the characterLayers received from the front (as an array of string) with the custom textures from the back. */ - static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] { + static mergeCharacterLayersAndCustomTextures( + characterLayers: string[], + memberTextures: CharacterTexture[] + ): CharacterLayer[] { const characterLayerObjs: CharacterLayer[] = []; for (const characterLayer of characterLayers) { - if (characterLayer.startsWith('customCharacterTexture')) { + if (characterLayer.startsWith("customCharacterTexture")) { const customCharacterLayerId: number = +characterLayer.substr(22); for (const memberTexture of memberTextures) { if (memberTexture.id == customCharacterLayerId) { characterLayerObjs.push({ name: characterLayer, - url: memberTexture.url - }) + url: memberTexture.url, + }); break; } } } else { characterLayerObjs.push({ name: characterLayer, - url: undefined - }) + url: undefined, + }); } } return characterLayerObjs; @@ -572,7 +589,7 @@ export class SocketManager implements ZoneEventListener { emitInBatch(listener, subMessage); } - + public emitWorldFullMessage(client: WebSocket) { const errorMessage = new WorldFullMessage(); @@ -594,9 +611,9 @@ export class SocketManager implements ZoneEventListener { private refreshRoomData(roomId: string, versionNumber: number): void { const room = this.rooms.get(roomId); - //this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed. + //this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed. if (!room || !room.needsUpdate(versionNumber)) return; - + this.updateRoomWithAdminData(room); } From 3e464580ea04c54b75fb3bd78e940ea09eeb57b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 24 Jun 2021 10:23:32 +0200 Subject: [PATCH 87/87] Fixing eslint ignore broken by Prettier --- back/package.json | 4 ++-- back/src/Server/server/utils.ts | 2 +- pusher/src/Server/server/utils.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/back/package.json b/back/package.json index 5bf5d031..7015b9b8 100644 --- a/back/package.json +++ b/back/package.json @@ -10,8 +10,8 @@ "runprod": "node --max-old-space-size=4096 ./dist/server.js", "profile": "tsc && node --prof ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", - "lint": "node_modules/.bin/eslint src/ . --ext .ts", - "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", + "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", + "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", "precommit": "lint-staged", "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" diff --git a/back/src/Server/server/utils.ts b/back/src/Server/server/utils.ts index 9816de54..dc813064 100644 --- a/back/src/Server/server/utils.ts +++ b/back/src/Server/server/utils.ts @@ -1,7 +1,7 @@ import { ReadStream } from "fs"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any function extend(who: any, from: any, overwrite = true) { - // eslint-disable-line @typescript-eslint/no-explicit-any const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); ownProps.forEach((prop) => { if (prop === "constructor" || from[prop] === undefined) return; diff --git a/pusher/src/Server/server/utils.ts b/pusher/src/Server/server/utils.ts index 9816de54..dc813064 100644 --- a/pusher/src/Server/server/utils.ts +++ b/pusher/src/Server/server/utils.ts @@ -1,7 +1,7 @@ import { ReadStream } from "fs"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any function extend(who: any, from: any, overwrite = true) { - // eslint-disable-line @typescript-eslint/no-explicit-any const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); ownProps.forEach((prop) => { if (prop === "constructor" || from[prop] === undefined) return;