diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 1ad73027..ea67c3c1 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -39,7 +39,7 @@ jobs: working-directory: "messages" - name: "Build proto messages" - run: yarn run proto && yarn run copy-to-front && yarn run json-copy-to-front + run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front working-directory: "messages" - name: "Create index.html" diff --git a/.github/workflows/end_to_end_tests.yml b/.github/workflows/end_to_end_tests.yml index a1cb5e5d..ea9ba41c 100644 --- a/.github/workflows/end_to_end_tests.yml +++ b/.github/workflows/end_to_end_tests.yml @@ -12,6 +12,7 @@ on: jobs: start-runner: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) name: Start self-hosted EC2 runner runs-on: ubuntu-latest outputs: @@ -109,12 +110,14 @@ jobs: if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs steps: - name: Configure AWS credentials + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ secrets.AWS_REGION }} - name: Stop EC2 runner + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) uses: machulav/ec2-github-runner@v2 with: mode: stop diff --git a/.github/workflows/push-to-npm.yml b/.github/workflows/push-to-npm.yml index 71f2824f..571a16e6 100644 --- a/.github/workflows/push-to-npm.yml +++ b/.github/workflows/push-to-npm.yml @@ -36,7 +36,7 @@ jobs: working-directory: "messages" - name: "Build proto messages" - run: yarn run proto && yarn run copy-to-front && yarn run json-copy-to-front + run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front working-directory: "messages" - name: "Create index.html" diff --git a/back/server.ts b/back/server.ts index 4ed443b8..1e474ec9 100644 --- a/back/server.ts +++ b/back/server.ts @@ -1,15 +1,15 @@ // lib/server.ts import App from "./src/App"; import grpc from "grpc"; -import {roomManager} from "./src/RoomManager"; -import {IRoomManagerServer, RoomManagerService} from "./src/Messages/generated/messages_grpc_pb"; -import {HTTP_PORT, GRPC_PORT} from "./src/Enum/EnvironmentVariable"; +import { roomManager } from "./src/RoomManager"; +import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb"; +import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable"; -App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT)) +App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT)); const server = new grpc.Server(); server.addService<IRoomManagerServer>(RoomManagerService, roomManager); server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure()); server.start(); -console.log('WorkAdventure HTTP/2 API starting on port %d!', GRPC_PORT); +console.log("WorkAdventure HTTP/2 API starting on port %d!", GRPC_PORT); diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 3bb425b7..d375fbd8 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -106,11 +106,6 @@ const roomManager: IRoomManagerServer = { user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage ); - } else if (message.hasPlayglobalmessage()) { - socketManager.emitPlayGlobalMessage( - room, - message.getPlayglobalmessage() as PlayGlobalMessage - ); } else if (message.hasQueryjitsijwtmessage()) { socketManager.handleQueryJitsiJwtMessage( user, diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index c9da7c96..9233811b 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -531,15 +531,6 @@ export class SocketManager { } } - emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) { - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setPlayglobalmessage(playGlobalMessage); - - for (const [id, user] of room.getUsers().entries()) { - user.socket.write(serverToClientMessage); - } - } - public getWorlds(): Map<string, PromiseLike<GameRoom>> { return this.roomsPromises; } diff --git a/docs/maps/api-camera.md b/docs/maps/api-camera.md new file mode 100644 index 00000000..cb1fe72d --- /dev/null +++ b/docs/maps/api-camera.md @@ -0,0 +1,24 @@ +{.section-title.accent.text-primary} +# API Camera functions Reference + +### Listen to camera updates + +``` +WA.camera.onCameraUpdate(): Subscription +``` + +Listens to updates of the camera viewport. It will trigger for every update of the camera's properties (position or scale for instance). An event will be sent. + +The event has the following attributes : +* **x (number):** coordinate X of the camera's world view (the area looked at by the camera). +* **y (number):** coordinate Y of the camera's world view. +* **width (number):** the width of the camera's world view. +* **height (number):** the height of the camera's world view. + +**callback:** the function that will be called when the camera is updated. + +Example : +```javascript +const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView)); +//later... +subscription.unsubscribe(); \ No newline at end of file diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index 35d5f464..58d5701a 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -86,6 +86,27 @@ WA.onInit().then(() => { }) ``` +### Get the position of the player +``` +WA.player.getPosition(): Promise<Position> +``` +The player's current position is available using the `WA.player.getPosition()` function. + +`Position` has the following attributes : +* **x (number) :** The coordinate x of the current player's position. +* **y (number) :** The coordinate y of the current player's position. + + +{.alert.alert-info} +You need to wait for the end of the initialization before calling `WA.player.getPosition()` + +```typescript +WA.onInit().then(async () => { + console.log('Position: ', await WA.player.getPosition()); +}) +``` + + ### Listen to player movement ``` WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; @@ -107,6 +128,30 @@ Example : WA.player.onPlayerMove(console.log); ``` +## Player specific variables +Similarly to maps (see [API state related functions](api-state.md)), it is possible to store data **related to a specific player** in a "state". Such data will be stored using the local storage from the user's browser. Any value that is serializable in JSON can be stored. + +{.alert.alert-info} +In the future, player-related variables will be stored on the WorkAdventure server if the current player is logged. + +Any value that is serializable in JSON can be stored. + +### Setting a property +A player property can be set simply by assigning a value. + +Example: +```javascript +WA.player.state.toto = "value" //will set the "toto" key to "value" +``` + +### Reading a variable +A player variable can be read by calling its key from the player's state. + +Example: +```javascript +WA.player.state.toto //will retrieve the variable +``` + ### Set the outline color of the player ``` WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>; diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index d044668f..a0869075 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -10,5 +10,6 @@ - [UI functions](api-ui.md) - [Sound functions](api-sound.md) - [Controls functions](api-controls.md) +- [Camera functions](api-camera.md) - [List of deprecated functions](api-deprecated.md) diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index 72947df8..7d438a1f 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -1,8 +1,11 @@ {.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. + +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 : <div class="row"> @@ -12,6 +15,7 @@ Example : </div> The name of the layers of this map are : + * `entries/start` * `bottom/ground/under` * `bottom/build/carpet` @@ -26,29 +30,32 @@ WA.room.onLeaveLayer(name: string): Subscription Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer. -* **name**: the name of the layer who as defined in Tiled. +* **name**: the name of the layer who as defined in Tiled. Example: ```javascript WA.room.onEnterLayer('myLayer').subscribe(() => { - WA.chat.sendChatMessage("Hello!", 'Mr Robot'); + WA.chat.sendChatMessage("Hello!", 'Mr Robot'); }); WA.room.onLeaveLayer('myLayer').subscribe(() => { - WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); + 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. -if `layerName` is the name of a group layer, show/hide all the layer in that group layer. + +These 2 methods can be used to show and hide a layer. if `layerName` is the name of a group layer, show/hide all the +layer in that group layer. Example : + ```javascript WA.room.showLayer('bottom'); //... @@ -61,12 +68,14 @@ WA.room.hideLayer('bottom'); 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`. +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`. Note : To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`. Example : + ```javascript WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` @@ -79,13 +88,12 @@ WA.room.id: string; The ID of the current room is available from the `WA.room.id` property. -{.alert.alert-info} -You need to wait for the end of the initialization before accessing `WA.room.id` +{.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.id` ```typescript WA.onInit().then(() => { - console.log('Room id: ', WA.room.id); - // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json" + console.log('Room id: ', WA.room.id); + // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json" }) ``` @@ -97,19 +105,17 @@ WA.room.mapURL: string; The URL of the map is available from the `WA.room.mapURL` property. -{.alert.alert-info} -You need to wait for the end of the initialization before accessing `WA.room.mapURL` +{.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.mapURL` ```typescript WA.onInit().then(() => { - console.log('Map URL: ', WA.room.mapURL); - // Will output something like: 'https://mymap.org/map.json" + console.log('Map URL: ', WA.room.mapURL); + // Will output something like: 'https://mymap.org/map.json" }) ``` - - ### Getting map data + ``` WA.room.getTiledMap(): Promise<ITiledMap> ``` @@ -121,12 +127,16 @@ const map = await WA.room.getTiledMap(); console.log("Map generated with Tiled version ", map.tiledversion); ``` -Check the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/). +Check +the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/) +. ### Changing tiles + ``` WA.room.setTiles(tiles: TileDescriptor[]): void ``` + Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`. If `tile` is a string, it's not the id of the tile but the value of the property `name`. @@ -137,43 +147,48 @@ If `tile` is a string, it's not the id of the tile but the value of the property </div> `TileDescriptor` has the following attributes : + * **x (number) :** The coordinate x of the tile that you want to replace. * **y (number) :** The coordinate y of the tile that you want to replace. * **tile (number | string) :** The id of the tile that will be placed in the map. * **layer (string) :** The name of the layer where the tile will be placed. -**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor. +**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want +to the id of the tile in Tiled Editor. Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`. Example : + ```javascript WA.room.setTiles([ - {x: 6, y: 4, tile: 'blue', layer: 'setTiles'}, - {x: 7, y: 4, tile: 109, layer: 'setTiles'}, - {x: 8, y: 4, tile: 109, layer: 'setTiles'}, - {x: 9, y: 4, tile: 'blue', layer: 'setTiles'} - ]); + { x: 6, y: 4, tile: 'blue', layer: 'setTiles' }, + { x: 7, y: 4, tile: 109, layer: 'setTiles' }, + { x: 8, y: 4, tile: 109, layer: 'setTiles' }, + { x: 9, y: 4, tile: 'blue', layer: 'setTiles' } +]); ``` ### Loading a tileset + ``` WA.room.loadTileset(url: string): Promise<number> ``` + Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset. You can create a tileset file in Tile Editor. ```javascript WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { - WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]); + WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: 'bottom' }]); }) ``` - ## Embedding websites in a map -You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using the ["website" objects](website-in-map.md)). +You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using +the ["website" objects](website-in-map.md)). ### Getting an instance of a website already embedded in the map @@ -181,8 +196,8 @@ You can use the scripting API to embed websites in a map, or to edit websites th WA.room.website.get(objectName: string): Promise<EmbeddedWebsite> ``` -You can get an instance of an embedded website by using the `WA.room.website.get()` method. -It returns a promise of an `EmbeddedWebsite` instance. +You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of +an `EmbeddedWebsite` instance. ```javascript // Get an existing website object where 'my_website' is the name of the object (on any layer object of the map) @@ -191,7 +206,6 @@ website.url = 'https://example.com'; website.visible = true; ``` - ### Adding a new website in a map ``` @@ -201,34 +215,38 @@ interface CreateEmbeddedWebsiteEvent { name: string; // A unique name for this iframe url: string; // The URL the iframe points to. position: { - x: number, // In pixels, relative to the map coordinates - y: number, // In pixels, relative to the map coordinates - width: number, // In pixels, sensitive to zoom level - height: number, // In pixels, sensitive to zoom level + x: number, // In "game" pixels, relative to the map or player coordinates, depending on origin + y: number, // In "game" pixels, relative to the map or player coordinates, depending on origin + width: number, // In "game" pixels + height: number, // In "game" pixels }, visible?: boolean, // Whether to display the iframe or not allowApi?: boolean, // Whether the scripting API should be available to the iframe allow?: string, // The list of feature policies allowed + origin: "player" | "map" // The origin used to place the x and y coordinates of the iframe's top-left corner, defaults to "map" + scale: number, // A ratio used to resize the iframe } ``` -You can create an instance of an embedded website by using the `WA.room.website.create()` method. -It returns an `EmbeddedWebsite` instance. +You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns +an `EmbeddedWebsite` instance. ```javascript // Create a new website object const website = WA.room.website.create({ - name: "my_website", - url: "https://example.com", - position: { - x: 64, - y: 128, - width: 320, - height: 240, - }, - visible: true, - allowApi: true, - allow: "fullscreen", + name: "my_website", + url: "https://example.com", + position: { + x: 64, + y: 128, + width: 320, + height: 240, + }, + visible: true, + allowApi: true, + allow: "fullscreen", + origin: "map", + scale: 1, }); ``` @@ -240,30 +258,28 @@ WA.room.website.delete(name: string): Promise<void> Use `WA.room.website.delete` to completely remove an embedded website from your map. - ### The EmbeddedWebsite class Instances of the `EmbeddedWebsite` class represent the website displayed on the map. ```typescript class EmbeddedWebsite { - readonly name: string; - url: string; - visible: boolean; - allow: string; - allowApi: boolean; - x: number; // In pixels, relative to the map coordinates - y: number; // In pixels, relative to the map coordinates - width: number; // In pixels, sensitive to zoom level - height: number; // In pixels, sensitive to zoom level + readonly name: string; + url: string; + visible: boolean; + allow: string; + allowApi: boolean; + x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin + y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin + width: number; // In "game" pixels + height: number; // In "game" pixels + origin: "player" | "map"; + scale: number; } ``` When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map. - -{.alert.alert-warning} -The websites you add/edit/delete via the scripting API are only shown locally. If you want them -to be displayed for every player, you can use [variables](api-start.md) to share a common state -between all users. +{.alert.alert-warning} The websites you add/edit/delete via the scripting API are only shown locally. If you want them +to be displayed for every player, you can use [variables](api-start.md) to share a common state between all users. diff --git a/front/.eslintrc.js b/front/.eslintrc.js index 117cb7e6..dc2b6bd6 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -35,7 +35,6 @@ module.exports = { "no-unused-vars": "off", "@typescript-eslint/no-explicit-any": "error", // TODO: remove those ignored rules and write a stronger code! - "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/restrict-plus-operands": "off", "@typescript-eslint/no-unsafe-assignment": "off", diff --git a/front/Dockerfile b/front/Dockerfile index f781a37c..49cf6046 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,13 +1,14 @@ FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder WORKDIR /usr/src COPY messages . -RUN yarn install && yarn proto +RUN yarn install && yarn ts-proto # we are rebuilding on each deploy to cope with the PUSHER_URL environment URL FROM thecodingmachine/nodejs:14-apache COPY --chown=docker:docker front . -COPY --from=builder --chown=docker:docker /usr/src/generated /var/www/html/src/Messages/generated +COPY --from=builder --chown=docker:docker /usr/src/ts-proto-generated/protos /var/www/html/src/Messages/ts-proto-generated +RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' /var/www/html/src/Messages/ts-proto-generated/messages.ts COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages # Removing the iframe.html file from the final image as this adds a XSS attack. diff --git a/front/package.json b/front/package.json index eae92cd2..935c254f 100644 --- a/front/package.json +++ b/front/package.json @@ -62,6 +62,7 @@ "simple-peer": "^9.11.0", "socket.io-client": "^2.3.0", "standardized-audio-context": "^25.2.4", + "ts-proto": "^1.96.0", "uuidv4": "^6.2.10" }, "scripts": { diff --git a/front/src/Administration/AnalyticsClient.ts b/front/src/Administration/AnalyticsClient.ts index fb2b604b..4c1ca93a 100644 --- a/front/src/Administration/AnalyticsClient.ts +++ b/front/src/Administration/AnalyticsClient.ts @@ -18,64 +18,84 @@ class AnalyticsClient { } identifyUser(uuid: string, email: string | null) { - this.posthogPromise?.then((posthog) => { - posthog.identify(uuid, { uuid, email, wa: true }); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.identify(uuid, { uuid, email, wa: true }); + }) + .catch((e) => console.error(e)); } loggedWithSso() { - this.posthogPromise?.then((posthog) => { - posthog.capture("wa-logged-sso"); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-logged-sso"); + }) + .catch((e) => console.error(e)); } loggedWithToken() { - this.posthogPromise?.then((posthog) => { - posthog.capture("wa-logged-token"); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-logged-token"); + }) + .catch((e) => console.error(e)); } enteredRoom(roomId: string, roomGroup: string | null) { - this.posthogPromise?.then((posthog) => { - posthog.capture("$pageView", { roomId, roomGroup }); - posthog.capture("enteredRoom"); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("$pageView", { roomId, roomGroup }); + posthog.capture("enteredRoom"); + }) + .catch((e) => console.error(e)); } openedMenu() { - this.posthogPromise?.then((posthog) => { - posthog.capture("wa-opened-menu"); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-opened-menu"); + }) + .catch((e) => console.error(e)); } launchEmote(emote: string) { - this.posthogPromise?.then((posthog) => { - posthog.capture("wa-emote-launch", { emote }); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-emote-launch", { emote }); + }) + .catch((e) => console.error(e)); } enteredJitsi(roomName: string, roomId: string) { - this.posthogPromise?.then((posthog) => { - posthog.capture("wa-entered-jitsi", { roomName, roomId }); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-entered-jitsi", { roomName, roomId }); + }) + .catch((e) => console.error(e)); } validationName() { - this.posthogPromise?.then((posthog) => { - posthog.capture("wa-name-validation"); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-name-validation"); + }) + .catch((e) => console.error(e)); } validationWoka(scene: string) { - this.posthogPromise?.then((posthog) => { - posthog.capture("wa-woka-validation", { scene }); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-woka-validation", { scene }); + }) + .catch((e) => console.error(e)); } validationVideo() { - this.posthogPromise?.then((posthog) => { - posthog.capture("wa-video-validation"); - }); + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-video-validation"); + }) + .catch((e) => console.error(e)); } } export const analyticsClient = new AnalyticsClient(); diff --git a/front/src/Api/Events/EmbeddedWebsiteEvent.ts b/front/src/Api/Events/EmbeddedWebsiteEvent.ts index 42630be1..57c24853 100644 --- a/front/src/Api/Events/EmbeddedWebsiteEvent.ts +++ b/front/src/Api/Events/EmbeddedWebsiteEvent.ts @@ -22,6 +22,8 @@ export const isEmbeddedWebsiteEvent = new tg.IsInterface() y: tg.isNumber, width: tg.isNumber, height: tg.isNumber, + origin: tg.isSingletonStringUnion("player", "map"), + scale: tg.isNumber, }) .get(); @@ -35,6 +37,8 @@ export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface() visible: tg.isBoolean, allowApi: tg.isBoolean, allow: tg.isString, + origin: tg.isSingletonStringUnion("player", "map"), + scale: tg.isNumber, }) .get(); diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 1f0f36ed..9755ba9e 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -10,6 +10,7 @@ export const isGameStateEvent = new tg.IsInterface() tags: tg.isArray(tg.isString), variables: tg.isObject, userRoomToken: tg.isUnion(tg.isString, tg.isUndefined), + playerVariables: tg.isObject, }) .get(); /** diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 2871b93c..8fb488dc 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -30,6 +30,8 @@ import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEv import type { ChangeLayerEvent } from "./ChangeLayerEvent"; import type { ChangeZoneEvent } from "./ChangeZoneEvent"; import { isColorEvent } from "./ColorEvent"; +import { isPlayerPosition } from "./PlayerPosition"; +import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent"; export interface TypedMessageEvent<T> extends MessageEvent { data: T; @@ -50,6 +52,7 @@ export type IframeEventMap = { displayBubble: null; removeBubble: null; onPlayerMove: undefined; + onCameraUpdate: undefined; showLayer: LayerEvent; hideLayer: LayerEvent; setProperty: SetPropertyEvent; @@ -82,6 +85,7 @@ export interface IframeResponseEventMap { leaveZoneEvent: ChangeZoneEvent; buttonClickedEvent: ButtonClickedEvent; hasPlayerMoved: HasPlayerMovedEvent; + wasCameraUpdated: WasCameraUpdatedEvent; menuItemClicked: MenuItemClickedEvent; setVariable: SetVariableEvent; messageTriggered: MessageReferenceEvent; @@ -161,6 +165,10 @@ export const iframeQueryMapTypeGuards = { query: tg.isUndefined, answer: tg.isUndefined, }, + getPlayerPosition: { + query: tg.isUndefined, + answer: isPlayerPosition, + }, }; type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never; diff --git a/front/src/Api/Events/PlayerPosition.ts b/front/src/Api/Events/PlayerPosition.ts new file mode 100644 index 00000000..54fac6fe --- /dev/null +++ b/front/src/Api/Events/PlayerPosition.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isPlayerPosition = new tg.IsInterface() + .withProperties({ + x: tg.isNumber, + y: tg.isNumber, + }) + .get(); + +export type PlayerPosition = tg.GuardedType<typeof isPlayerPosition>; diff --git a/front/src/Api/Events/SetVariableEvent.ts b/front/src/Api/Events/SetVariableEvent.ts index 3e2303b3..80ac6f6e 100644 --- a/front/src/Api/Events/SetVariableEvent.ts +++ b/front/src/Api/Events/SetVariableEvent.ts @@ -4,6 +4,7 @@ export const isSetVariableEvent = new tg.IsInterface() .withProperties({ key: tg.isString, value: tg.isUnknown, + target: tg.isSingletonStringUnion("global", "player"), }) .get(); /** diff --git a/front/src/Api/Events/WasCameraUpdatedEvent.ts b/front/src/Api/Events/WasCameraUpdatedEvent.ts new file mode 100644 index 00000000..34e39a84 --- /dev/null +++ b/front/src/Api/Events/WasCameraUpdatedEvent.ts @@ -0,0 +1,19 @@ +import * as tg from "generic-type-guard"; + +export const isWasCameraUpdatedEvent = new tg.IsInterface() + .withProperties({ + x: tg.isNumber, + y: tg.isNumber, + width: tg.isNumber, + height: tg.isNumber, + zoom: tg.isNumber, + }) + .get(); + +/** + * A message sent from the game to the iFrame to notify a movement from the camera. + */ + +export type WasCameraUpdatedEvent = tg.GuardedType<typeof isWasCameraUpdatedEvent>; + +export type WasCameraUpdatedEventCallback = (event: WasCameraUpdatedEvent) => void; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 67b49344..216a9510 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -31,6 +31,7 @@ import type { SetVariableEvent } from "./Events/SetVariableEvent"; import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore"; import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent"; +import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent"; import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent"; type AnswererCallback<T extends keyof IframeQueryMap> = ( @@ -85,6 +86,9 @@ class IframeListener { private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject(); public readonly loadSoundStream = this._loadSoundStream.asObservable(); + private readonly _trackCameraUpdateStream: Subject<LoadSoundEvent> = new Subject(); + public readonly trackCameraUpdateStream = this._trackCameraUpdateStream.asObservable(); + private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject(); public readonly setTilesStream = this._setTilesStream.asObservable(); @@ -226,6 +230,8 @@ class IframeListener { this._removeBubbleStream.next(); } else if (payload.type == "onPlayerMove") { this.sendPlayerMove = true; + } else if (payload.type == "onCameraUpdate") { + this._trackCameraUpdateStream.next(); } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { this._setTilesStream.next(payload.data); } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { @@ -442,6 +448,13 @@ class IframeListener { } } + sendCameraUpdated(event: WasCameraUpdatedEvent) { + this.postMessage({ + type: "wasCameraUpdated", + data: event, + }); + } + sendButtonClickedEvent(popupId: number, buttonId: number): void { this.postMessage({ type: "buttonClickedEvent", diff --git a/front/src/Api/iframe/Room/EmbeddedWebsite.ts b/front/src/Api/iframe/Room/EmbeddedWebsite.ts index 7b16890e..d9c2d986 100644 --- a/front/src/Api/iframe/Room/EmbeddedWebsite.ts +++ b/front/src/Api/iframe/Room/EmbeddedWebsite.ts @@ -12,6 +12,8 @@ export class EmbeddedWebsite { private _allow: string; private _allowApi: boolean; private _position: Rectangle; + private readonly origin: "map" | "player" | undefined; + private _scale: number; constructor(private config: CreateEmbeddedWebsiteEvent) { this.name = config.name; @@ -20,6 +22,12 @@ export class EmbeddedWebsite { this._allow = config.allow ?? ""; this._allowApi = config.allowApi ?? false; this._position = config.position; + this.origin = config.origin; + this._scale = config.scale ?? 1; + } + + public get url() { + return this._url; } public set url(url: string) { @@ -33,6 +41,10 @@ export class EmbeddedWebsite { }); } + public get visible() { + return this._visible; + } + public set visible(visible: boolean) { this._visible = visible; sendToWorkadventure({ @@ -44,6 +56,10 @@ export class EmbeddedWebsite { }); } + public get x() { + return this._position.x; + } + public set x(x: number) { this._position.x = x; sendToWorkadventure({ @@ -55,6 +71,10 @@ export class EmbeddedWebsite { }); } + public get y() { + return this._position.y; + } + public set y(y: number) { this._position.y = y; sendToWorkadventure({ @@ -66,6 +86,10 @@ export class EmbeddedWebsite { }); } + public get width() { + return this._position.width; + } + public set width(width: number) { this._position.width = width; sendToWorkadventure({ @@ -77,6 +101,10 @@ export class EmbeddedWebsite { }); } + public get height() { + return this._position.height; + } + public set height(height: number) { this._position.height = height; sendToWorkadventure({ @@ -87,4 +115,19 @@ export class EmbeddedWebsite { }, }); } + + public get scale(): number { + return this._scale; + } + + public set scale(scale: number) { + this._scale = scale; + sendToWorkadventure({ + type: "modifyEmbeddedWebsite", + data: { + name: this.name, + scale: this._scale, + }, + }); + } } diff --git a/front/src/Api/iframe/Ui/ActionMessage.ts b/front/src/Api/iframe/Ui/ActionMessage.ts index 912603b9..ff0908ff 100644 --- a/front/src/Api/iframe/Ui/ActionMessage.ts +++ b/front/src/Api/iframe/Ui/ActionMessage.ts @@ -26,7 +26,7 @@ export class ActionMessage { this.message = actionMessageOptions.message; this.type = actionMessageOptions.type ?? "message"; this.callback = actionMessageOptions.callback; - this.create(); + this.create().catch((e) => console.error(e)); } private async create() { diff --git a/front/src/Api/iframe/camera.ts b/front/src/Api/iframe/camera.ts new file mode 100644 index 00000000..a832290e --- /dev/null +++ b/front/src/Api/iframe/camera.ts @@ -0,0 +1,29 @@ +import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; +import { Subject } from "rxjs"; +import type { WasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent"; +import { apiCallback } from "./registeredCallbacks"; +import { isWasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent"; + +const moveStream = new Subject<WasCameraUpdatedEvent>(); + +export class WorkAdventureCameraCommands extends IframeApiContribution<WorkAdventureCameraCommands> { + callbacks = [ + apiCallback({ + type: "wasCameraUpdated", + typeChecker: isWasCameraUpdatedEvent, + callback: (payloadData) => { + moveStream.next(payloadData); + }, + }), + ]; + + onCameraUpdate(): Subject<WasCameraUpdatedEvent> { + sendToWorkadventure({ + type: "onCameraUpdate", + data: null, + }); + return moveStream; + } +} + +export default new WorkAdventureCameraCommands(); diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts index 2d187bf5..0c71ae33 100644 --- a/front/src/Api/iframe/player.ts +++ b/front/src/Api/iframe/player.ts @@ -3,6 +3,7 @@ import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events import { Subject } from "rxjs"; import { apiCallback } from "./registeredCallbacks"; import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent"; +import { createState } from "./state"; const moveStream = new Subject<HasPlayerMovedEvent>(); @@ -31,6 +32,8 @@ export const setUuid = (_uuid: string | undefined) => { }; export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> { + readonly state = createState("player"); + callbacks = [ apiCallback({ type: "hasPlayerMoved", @@ -74,6 +77,13 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven return uuid; } + async getPosition(): Promise<Position> { + return await queryWorkadventure({ + type: "getPlayerPosition", + data: undefined, + }); + } + get userRoomToken(): string | undefined { if (userRoomToken === undefined) { throw new Error( @@ -102,4 +112,9 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven } } +export type Position = { + x: number; + y: number; +}; + export default new WorkadventurePlayerCommands(); diff --git a/front/src/Api/iframe/state.ts b/front/src/Api/iframe/state.ts index a875f3e0..278b208e 100644 --- a/front/src/Api/iframe/state.ts +++ b/front/src/Api/iframe/state.ts @@ -8,93 +8,101 @@ import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; -const setVariableResolvers = new Subject<SetVariableEvent>(); -const variables = new Map<string, unknown>(); -const variableSubscribers = new Map<string, Subject<unknown>>(); - -export const initVariables = (_variables: Map<string, unknown>): void => { - for (const [name, value] of _variables.entries()) { - // In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this. - if (!variables.has(name)) { - variables.set(name, value); - } - } -}; - -setVariableResolvers.subscribe((event) => { - const oldValue = variables.get(event.key); - // If we are setting the same value, no need to do anything. - // No need to do this check since it is already performed in SharedVariablesManager - /*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) { - return; - }*/ - - variables.set(event.key, event.value); - const subject = variableSubscribers.get(event.key); - if (subject !== undefined) { - subject.next(event.value); - } -}); - export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> { + private setVariableResolvers = new Subject<SetVariableEvent>(); + private variables = new Map<string, unknown>(); + private variableSubscribers = new Map<string, Subject<unknown>>(); + + constructor(private target: "global" | "player") { + super(); + + this.setVariableResolvers.subscribe((event) => { + const oldValue = this.variables.get(event.key); + // If we are setting the same value, no need to do anything. + // No need to do this check since it is already performed in SharedVariablesManager + /*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) { + return; + }*/ + + this.variables.set(event.key, event.value); + const subject = this.variableSubscribers.get(event.key); + if (subject !== undefined) { + subject.next(event.value); + } + }); + } + callbacks = [ apiCallback({ type: "setVariable", typeChecker: isSetVariableEvent, callback: (payloadData) => { - setVariableResolvers.next(payloadData); + if (payloadData.target === this.target) { + this.setVariableResolvers.next(payloadData); + } }, }), ]; + // TODO: see how we can remove this method from types exposed to WA.state object + initVariables(_variables: Map<string, unknown>): void { + for (const [name, value] of _variables.entries()) { + // In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this. + if (!this.variables.has(name)) { + this.variables.set(name, value); + } + } + } + saveVariable(key: string, value: unknown): Promise<void> { - variables.set(key, value); + this.variables.set(key, value); return queryWorkadventure({ type: "setVariable", data: { key, value, + target: this.target, }, }); } loadVariable(key: string): unknown { - return variables.get(key); + return this.variables.get(key); } hasVariable(key: string): boolean { - return variables.has(key); + return this.variables.has(key); } onVariableChange(key: string): Observable<unknown> { - let subject = variableSubscribers.get(key); + let subject = this.variableSubscribers.get(key); if (subject === undefined) { subject = new Subject<unknown>(); - variableSubscribers.set(key, subject); + this.variableSubscribers.set(key, subject); } return subject.asObservable(); } } -const proxyCommand = new Proxy(new WorkadventureStateCommands(), { - get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown { - if (p in target) { - return Reflect.get(target, p, receiver); - } - return target.loadVariable(p.toString()); - }, - set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean { - // Note: when using "set", there is no way to wait, so we ignore the return of the promise. - // User must use WA.state.saveVariable to have error message. - target.saveVariable(p.toString(), value); - return true; - }, - has(target: WorkadventureStateCommands, p: PropertyKey): boolean { - if (p in target) { +export function createState(target: "global" | "player"): WorkadventureStateCommands & { [key: string]: unknown } { + return new Proxy(new WorkadventureStateCommands(target), { + get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown { + if (p in target) { + return Reflect.get(target, p, receiver); + } + return target.loadVariable(p.toString()); + }, + set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean { + // Note: when using "set", there is no way to wait, so we ignore the return of the promise. + // User must use WA.state.saveVariable to have error message. + target.saveVariable(p.toString(), value).catch((e) => console.error(e)); return true; - } - return target.hasVariable(p.toString()); - }, -}) as WorkadventureStateCommands & { [key: string]: unknown }; - -export default proxyCommand; + }, + has(target: WorkadventureStateCommands, p: PropertyKey): boolean { + if (p in target) { + return true; + } + return target.hasVariable(p.toString()); + }, + }) as WorkadventureStateCommands & { [key: string]: unknown }; +} diff --git a/front/src/Api/iframe/website.ts b/front/src/Api/iframe/website.ts index 28abb19a..eab1bce3 100644 --- a/front/src/Api/iframe/website.ts +++ b/front/src/Api/iframe/website.ts @@ -1,8 +1,4 @@ -import type { LoadSoundEvent } from "../Events/LoadSoundEvent"; -import type { PlaySoundEvent } from "../Events/PlaySoundEvent"; -import type { StopSoundEvent } from "../Events/StopSoundEvent"; import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution"; -import { Sound } from "./Sound/Sound"; import { EmbeddedWebsite } from "./Room/EmbeddedWebsite"; import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent"; diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index e9db2b77..a1277ed2 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -23,6 +23,9 @@ import { chatVisibilityStore } from "../Stores/ChatStore"; import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore"; import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte"; + import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore"; + import LimitRoomModal from "./Modal/LimitRoomModal.svelte"; + import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte"; import AudioPlaying from "./UI/AudioPlaying.svelte"; import { soundPlayingStore } from "../Stores/SoundPlayingStore"; import ErrorDialog from "./UI/ErrorDialog.svelte"; @@ -136,6 +139,16 @@ <HelpCameraSettingsPopup /> </div> {/if} + {#if $showLimitRoomModalStore} + <div> + <LimitRoomModal /> + </div> + {/if} + {#if $showShareLinkMapModalStore} + <div> + <ShareLinkMapModal /> + </div> + {/if} {#if $requestVisitCardsStore} <VisitCard visitCardUrl={$requestVisitCardsStore} /> {/if} diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte index 4422d002..b62d8fbe 100644 --- a/front/src/Components/AudioManager/AudioManager.svelte +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -19,12 +19,13 @@ audioManagerVolumeStore.setVolume(volume); audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted()); - unsubscriberFileStore = audioManagerFileStore.subscribe(() => { + unsubscriberFileStore = audioManagerFileStore.subscribe((src) => { HTMLAudioPlayer.pause(); + HTMLAudioPlayer.src = src; HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop; HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume; HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted; - HTMLAudioPlayer.play(); + void HTMLAudioPlayer.play(); }); unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => { const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking; @@ -148,9 +149,7 @@ </label> <section class="audio-manager-file"> <!-- svelte-ignore a11y-media-has-caption --> - <audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}> - <source src={$audioManagerFileStore} /> - </audio> + <audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer} /> </section> </div> </div> diff --git a/front/src/Components/Chat/ChatElement.svelte b/front/src/Components/Chat/ChatElement.svelte index 231e7260..4ac9c275 100644 --- a/front/src/Components/Chat/ChatElement.svelte +++ b/front/src/Components/Chat/ChatElement.svelte @@ -67,6 +67,7 @@ .messagePart { flex-grow: 1; max-width: 100%; + user-select: text; span.date { font-size: 80%; diff --git a/front/src/Components/FollowMenu/FollowMenu.svelte b/front/src/Components/FollowMenu/FollowMenu.svelte index 264b27ed..aaa23b3e 100644 --- a/front/src/Components/FollowMenu/FollowMenu.svelte +++ b/front/src/Components/FollowMenu/FollowMenu.svelte @@ -14,14 +14,11 @@ vim: ft=typescript } function sendFollowRequest() { - gameScene.connection?.emitFollowRequest(); - followRoleStore.set("leader"); - followStateStore.set("active"); + gameScene.CurrentPlayer.sendFollowRequest(); } function acceptFollowRequest() { - gameScene.CurrentPlayer.enableFollowing(); - gameScene.connection?.emitFollowConfirmation(); + gameScene.CurrentPlayer.startFollowing(); } function abortEnding() { @@ -42,23 +39,15 @@ vim: ft=typescript <svelte:window on:keydown={onKeyDown} /> -{#if $followStateStore === "requesting"} +{#if $followStateStore === "requesting" && $followRoleStore === "follower"} <div class="interact-menu nes-container is-rounded"> - {#if $followRoleStore === "follower"} - <section class="interact-menu-title"> - <h2>Do you want to follow {name($followUsersStore[0])}?</h2> - </section> - <section class="interact-menu-action"> - <button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest} - >Yes</button - > - <button type="button" class="nes-btn is-error" on:click|preventDefault={reset}>No</button> - </section> - {:else if $followRoleStore === "leader"} - <section class="interact-menu-question"> - <p>Should never be displayed</p> - </section> - {/if} + <section class="interact-menu-title"> + <h2>Do you want to follow {name($followUsersStore[0])}?</h2> + </section> + <section class="interact-menu-action"> + <button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}>Yes</button> + <button type="button" class="nes-btn is-error" on:click|preventDefault={reset}>No</button> + </section> </div> {/if} diff --git a/front/src/Components/Menu/GlobalMessagesSubMenu.svelte b/front/src/Components/Menu/GlobalMessagesSubMenu.svelte index 524e5e50..e755a243 100644 --- a/front/src/Components/Menu/GlobalMessagesSubMenu.svelte +++ b/front/src/Components/Menu/GlobalMessagesSubMenu.svelte @@ -19,12 +19,12 @@ uploadAudioActive = true; } - function send() { + async function send(): Promise<void> { if (inputSendTextActive) { - handleSendText.sendTextMessage(broadcastToWorld); + return handleSendText.sendTextMessage(broadcastToWorld); } if (uploadAudioActive) { - handleSendAudio.sendAudioMessage(broadcastToWorld); + return handleSendAudio.sendAudioMessage(broadcastToWorld); } } </script> diff --git a/front/src/Components/Menu/GuestSubMenu.svelte b/front/src/Components/Menu/GuestSubMenu.svelte index 0ae25b75..408dcbce 100644 --- a/front/src/Components/Menu/GuestSubMenu.svelte +++ b/front/src/Components/Menu/GuestSubMenu.svelte @@ -21,12 +21,12 @@ <div class="guest-main"> <section class="container-overflow"> <section class="share-url not-mobile"> - <h3>Share the link of the room !</h3> + <h3>Share the link of the room!</h3> <input type="text" readonly id="input-share-link" value={location.toString()} /> <button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button> </section> <section class="is-mobile"> - <h3>Share the link of the room !</h3> + <h3>Share the link of the room!</h3> <input type="hidden" readonly id="input-share-link" value={location.toString()} /> <button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button> </section> diff --git a/front/src/Components/Menu/MenuIcon.svelte b/front/src/Components/Menu/MenuIcon.svelte index bb5a2df2..9d3f72dd 100644 --- a/front/src/Components/Menu/MenuIcon.svelte +++ b/front/src/Components/Menu/MenuIcon.svelte @@ -1,9 +1,14 @@ <script lang="typescript"> import logoTalk from "../images/logo-message-pixel.png"; import logoWA from "../images/logo-WA-pixel.png"; + import logoInvite from "../images/logo-invite-pixel.png"; + import logoRegister from "../images/logo-register-pixel.png"; import { menuVisiblilityStore } from "../../Stores/MenuStore"; import { chatVisibilityStore } from "../../Stores/ChatStore"; + import { limitMapStore } from "../../Stores/GameStore"; import { get } from "svelte/store"; + import { ADMIN_URL } from "../../Enum/EnvironmentVariable"; + import { showShareLinkMapModalStore } from "../../Stores/ModalStore"; function showMenu() { menuVisiblilityStore.set(!get(menuVisiblilityStore)); @@ -11,13 +16,25 @@ function showChat() { chatVisibilityStore.set(true); } + + function register() { + window.open(`${ADMIN_URL}/second-step-register`, "_self"); + } + function showInvite() { + showShareLinkMapModalStore.set(true); + } </script> <svelte:window /> <main class="menuIcon"> - <img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} /> - <img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} /> + {#if $limitMapStore} + <img src={logoInvite} alt="open menu" class="nes-pointer" on:click|preventDefault={showInvite} /> + <img src={logoRegister} alt="open menu" class="nes-pointer" on:click|preventDefault={register} /> + {:else} + <img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} /> + <img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} /> + {/if} </main> <style lang="scss"> diff --git a/front/src/Components/Menu/ProfileSubMenu.svelte b/front/src/Components/Menu/ProfileSubMenu.svelte index 07356f6c..87bf57c9 100644 --- a/front/src/Components/Menu/ProfileSubMenu.svelte +++ b/front/src/Components/Menu/ProfileSubMenu.svelte @@ -41,10 +41,10 @@ gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene()); } - function logOut() { + async function logOut() { disableMenuStores(); loginSceneVisibleStore.set(true); - connectionManager.logout(); + return connectionManager.logout(); } function getProfileUrl() { diff --git a/front/src/Components/Menu/SettingsSubMenu.svelte b/front/src/Components/Menu/SettingsSubMenu.svelte index 1db14036..1ad1ac8b 100644 --- a/front/src/Components/Menu/SettingsSubMenu.svelte +++ b/front/src/Components/Menu/SettingsSubMenu.svelte @@ -33,9 +33,9 @@ const body = HtmlUtils.querySelectorOrFail("body"); if (body) { if (document.fullscreenElement !== null && !fullscreen) { - document.exitFullscreen(); + document.exitFullscreen().catch((e) => console.error(e)); } else { - body.requestFullscreen(); + body.requestFullscreen().catch((e) => console.error(e)); } localUserStore.setFullscreen(fullscreen); } @@ -45,14 +45,16 @@ if (Notification.permission === "granted") { localUserStore.setNotification(notification ? "granted" : "denied"); } else { - Notification.requestPermission().then((response) => { - if (response === "granted") { - localUserStore.setNotification(notification ? "granted" : "denied"); - } else { - localUserStore.setNotification("denied"); - notification = false; - } - }); + Notification.requestPermission() + .then((response) => { + if (response === "granted") { + localUserStore.setNotification(notification ? "granted" : "denied"); + } else { + localUserStore.setNotification("denied"); + notification = false; + } + }) + .catch((e) => console.error(e)); } } diff --git a/front/src/Components/Modal/LimitRoomModal.svelte b/front/src/Components/Modal/LimitRoomModal.svelte new file mode 100644 index 00000000..43941a6b --- /dev/null +++ b/front/src/Components/Modal/LimitRoomModal.svelte @@ -0,0 +1,47 @@ +<script lang="typescript"> + import { fly } from "svelte/transition"; + import { ADMIN_URL } from "../../Enum/EnvironmentVariable"; + + function register() { + window.open(`${ADMIN_URL}/second-step-register`, "_self"); + } +</script> + +<div class="limit-map nes-container" transition:fly={{ y: -900, duration: 500 }}> + <section> + <h2>Limit of your room</h2> + <p>Register your account!</p> + <p> + This map is limited in the time and to continue to use WorkAdventure, you must register your account in our + back office. + </p> + </section> + <section> + <button class="nes-btn is-primary" on:click|preventDefault={register}>Register</button> + </section> +</div> + +<style lang="scss"> + .limit-map { + pointer-events: auto; + background: #eceeee; + margin-left: auto; + margin-right: auto; + margin-top: 10vh; + max-height: 80vh; + max-width: 80vw; + overflow: auto; + text-align: center; + + h2 { + font-family: "Press Start 2P"; + } + + section { + p { + margin: 15px; + font-family: "Press Start 2P"; + } + } + } +</style> diff --git a/front/src/Components/Modal/ShareLinkMapModal.svelte b/front/src/Components/Modal/ShareLinkMapModal.svelte new file mode 100644 index 00000000..42ac1294 --- /dev/null +++ b/front/src/Components/Modal/ShareLinkMapModal.svelte @@ -0,0 +1,90 @@ +<script lang="typescript"> + import { fly } from "svelte/transition"; + import { showShareLinkMapModalStore } from "../../Stores/ModalStore"; + + interface ExtNavigator extends Navigator { + canShare?(data?: ShareData): Promise<boolean>; + } + + const myNavigator: ExtNavigator = window.navigator; + const haveNavigatorSharingFeature: boolean = + myNavigator && myNavigator.canShare != null && myNavigator.share != null; + + let copied: boolean = false; + + function copyLink() { + try { + const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement; + input.focus(); + input.select(); + document.execCommand("copy"); + copied = true; + } catch (e) { + console.error(e); + copied = false; + } + } + + async function shareLink() { + const shareData = { url: location.toString() }; + + try { + await myNavigator.share(shareData); + } catch (err) { + console.error("Error: " + err); + copyLink(); + } + } + + function close() { + showShareLinkMapModalStore.set(false); + copied = false; + } +</script> + +<div class="share-link-map nes-container" transition:fly={{ y: -900, duration: 500 }}> + <section> + <h2>Invite your friends or colleagues</h2> + <p>Share the link of the room!</p> + </section> + <section> + {#if haveNavigatorSharingFeature} + <input type="hidden" readonly id="input-share-link" value={location.toString()} /> + <button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button> + {:else} + <input type="text" readonly id="input-share-link" value={location.toString()} /> + <button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button> + {/if} + {#if copied} + <p>Copied!</p> + {/if} + </section> + <section> + <button class="nes-btn" on:click|preventDefault={close}>Close</button> + </section> +</div> + +<style lang="scss"> + div.share-link-map { + pointer-events: auto; + background: #eceeee; + margin-left: auto; + margin-right: auto; + margin-top: 10vh; + max-height: 80vh; + max-width: 80vw; + overflow: auto; + text-align: center; + + h2 { + font-family: "Press Start 2P"; + } + + section { + p { + margin: 15px; + font-family: "Press Start 2P"; + } + } + } +</style> diff --git a/front/src/Components/ReportMenu/ReportMenu.svelte b/front/src/Components/ReportMenu/ReportMenu.svelte index 8479a5b0..92601774 100644 --- a/front/src/Components/ReportMenu/ReportMenu.svelte +++ b/front/src/Components/ReportMenu/ReportMenu.svelte @@ -6,12 +6,11 @@ import type { Unsubscriber } from "svelte/store"; import { playersStore } from "../../Stores/PlayersStore"; import { connectionManager } from "../../Connexion/ConnectionManager"; - import { GameConnexionTypes } from "../../Url/UrlManager"; import { get } from "svelte/store"; let blockActive = true; let reportActive = !blockActive; - let anonymous: boolean = false; + let disableReport: boolean = false; let userUUID: string | undefined = playersStore.getPlayerById(get(showReportScreenStore).userId)?.userUuid; let userName = "No name"; let unsubscriber: Unsubscriber; @@ -26,7 +25,7 @@ } } }); - anonymous = connectionManager.getConnexionType === GameConnexionTypes.anonymous; + disableReport = !connectionManager.currentRoom?.canReport ?? true; }); onDestroy(() => { @@ -65,7 +64,7 @@ <button type="button" class="nes-btn" on:click|preventDefault={close}>X</button> </section> </section> - <section class="report-menu-action {anonymous ? 'hidden' : ''}"> + <section class="report-menu-action {disableReport ? 'hidden' : ''}"> <section class="justify-center"> <button type="button" diff --git a/front/src/Components/UI/AudioPlaying.svelte b/front/src/Components/UI/AudioPlaying.svelte index 09ffd639..a8d12ec9 100644 --- a/front/src/Components/UI/AudioPlaying.svelte +++ b/front/src/Components/UI/AudioPlaying.svelte @@ -12,7 +12,7 @@ } afterUpdate(() => { - audio.play(); + audio.play().catch((e) => console.error(e)); }); </script> diff --git a/front/src/Components/WarningContainer/WarningContainer.svelte b/front/src/Components/WarningContainer/WarningContainer.svelte index dd61d8fc..f75050fc 100644 --- a/front/src/Components/WarningContainer/WarningContainer.svelte +++ b/front/src/Components/WarningContainer/WarningContainer.svelte @@ -1,20 +1,26 @@ <script lang="typescript"> import { fly } from "svelte/transition"; - import { userIsAdminStore } from "../../Stores/GameStore"; + import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore"; import { ADMIN_URL } from "../../Enum/EnvironmentVariable"; const upgradeLink = ADMIN_URL + "/pricing"; + const registerLink = ADMIN_URL + "/second-step-register"; </script> <main class="warningMain" transition:fly={{ y: -200, duration: 500 }}> - <h2>Warning!</h2> {#if $userIsAdminStore} + <h2>Warning!</h2> <p> This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank" >here</a > </p> + {:else if $limitMapStore} + <p> + This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>! + </p> {:else} + <h2>Warning!</h2> <p>This world is close to its limit!</p> {/if} </main> diff --git a/front/src/Components/images/logo-invite-pixel.png b/front/src/Components/images/logo-invite-pixel.png new file mode 100644 index 00000000..d8ae6fff Binary files /dev/null and b/front/src/Components/images/logo-invite-pixel.png differ diff --git a/front/src/Components/images/logo-register-pixel.png b/front/src/Components/images/logo-register-pixel.png new file mode 100644 index 00000000..f9a45d3c Binary files /dev/null and b/front/src/Components/images/logo-register-pixel.png differ diff --git a/front/src/Connexion/AdminMessagesService.ts b/front/src/Connexion/AdminMessagesService.ts index 0b217760..4b7030ed 100644 --- a/front/src/Connexion/AdminMessagesService.ts +++ b/front/src/Connexion/AdminMessagesService.ts @@ -1,5 +1,5 @@ import { Subject } from "rxjs"; -import type { BanUserMessage, SendUserMessage } from "../Messages/generated/messages_pb"; +import type { BanUserMessage, SendUserMessage } from "../Messages/ts-proto-generated/messages"; export enum AdminMessageEventTypes { admin = "message", @@ -26,8 +26,8 @@ class AdminMessagesService { onSendusermessage(message: SendUserMessage | BanUserMessage) { this._messageStream.next({ - type: message.getType() as unknown as AdminMessageEventTypes, - text: message.getMessage(), + type: message.type as unknown as AdminMessageEventTypes, + text: message.message, }); } } diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 026cc20a..9c81fb25 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -8,12 +8,14 @@ import { CharacterTexture, LocalUser } from "./LocalUser"; import { Room } from "./Room"; import { _ServiceWorker } from "../Network/ServiceWorker"; import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore"; -import { userIsConnected } from "../Stores/MenuStore"; +import { userIsConnected, warningContainerStore } from "../Stores/MenuStore"; import { analyticsClient } from "../Administration/AnalyticsClient"; import { axiosWithRetry } from "./AxiosUtils"; import axios from "axios"; import { isRegisterData } from "../Messages/JsonMessages/RegisterData"; import { isAdminApiData } from "../Messages/JsonMessages/AdminApiData"; +import { limitMapStore } from "../Stores/GameStore"; +import { showLimitRoomModalStore } from "../Stores/ModalStore"; class ConnectionManager { private localUser!: LocalUser; @@ -152,11 +154,7 @@ class ConnectionManager { ) ); urlManager.pushRoomIdToUrl(this._currentRoom); - } else if ( - connexionType === GameConnexionTypes.organization || - connexionType === GameConnexionTypes.anonymous || - connexionType === GameConnexionTypes.empty - ) { + } else if (connexionType === GameConnexionTypes.room || connexionType === GameConnexionTypes.empty) { this.authToken = localUserStore.getAuthToken(); let roomPath: string; @@ -188,7 +186,7 @@ class ConnectionManager { //Set last room visited! (connected or nor, must to be saved in localstorage and cache API) //use href to keep # value - localUserStore.setLastRoomUrl(this._currentRoom.href); + await localUserStore.setLastRoomUrl(this._currentRoom.href); //todo: add here some kind of warning if authToken has expired. if (!this.authToken && !this._currentRoom.authenticationMandatory) { @@ -237,6 +235,17 @@ class ConnectionManager { analyticsClient.identifyUser(this.localUser.uuid, this.localUser.email); } + //if limit room active test headband + if (this._currentRoom.expireOn !== undefined) { + warningContainerStore.activateWarningContainer(); + limitMapStore.set(true); + + //check time of map + if (new Date() > this._currentRoom.expireOn) { + showLimitRoomModalStore.set(true); + } + } + this.serviceWorker = new _ServiceWorker(); return Promise.resolve(this._currentRoom); } @@ -280,7 +289,7 @@ class ConnectionManager { reject(error); }); - connection.onConnectingError((event: CloseEvent) => { + connection.connectionErrorStream.subscribe((event: CloseEvent) => { console.log("An error occurred while connecting to socket server. Retrying"); reject( new Error( @@ -292,7 +301,7 @@ class ConnectionManager { ); }); - connection.onConnect((connect: OnConnectInterface) => { + connection.roomJoinedMessageStream.subscribe((connect: OnConnectInterface) => { resolve(connect); }); }).catch((err) => { @@ -301,7 +310,7 @@ class ConnectionManager { this.reconnectingTimeout = setTimeout(() => { //todo: allow a way to break recursion? //todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely. - this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then( + void this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then( (connection) => resolve(connection) ); }, 4000 + Math.floor(Math.random() * 2000)); diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 6200e0c9..bf834a02 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -1,44 +1,12 @@ import type { SignalData } from "simple-peer"; import type { RoomConnection } from "./RoomConnection"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; - -export enum EventMessage { - CONNECT = "connect", - WEBRTC_SIGNAL = "webrtc-signal", - WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal", - WEBRTC_START = "webrtc-start", - //START_ROOM = "start-room", // From server to client: list of all room users/groups/items - JOIN_ROOM = "join-room", // bi-directional - USER_POSITION = "user-position", // From client to server - USER_MOVED = "user-moved", // From server to client - USER_LEFT = "user-left", // From server to client - MESSAGE_ERROR = "message-error", - WEBRTC_DISCONNECT = "webrtc-disconect", - GROUP_CREATE_UPDATE = "group-create-update", - GROUP_DELETE = "group-delete", - SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id. - ITEM_EVENT = "item-event", - USER_DETAILS_UPDATED = "user-details-updated", - - CONNECT_ERROR = "connect_error", - CONNECTING_ERROR = "connecting_error", - SET_SILENT = "set_silent", // Set or unset the silent mode for this user. - SET_VIEWPORT = "set-viewport", - BATCH = "batch", - - PLAY_GLOBAL_MESSAGE = "play-global-message", - STOP_GLOBAL_MESSAGE = "stop-global-message", - - TELEPORT = "teleport", - USER_MESSAGE = "user-message", - START_JITSI_ROOM = "start-jitsi-room", - SET_VARIABLE = "set-variable", -} +import { PositionMessage_Direction } from "../Messages/ts-proto-generated/messages"; export interface PointInterface { x: number; y: number; - direction: string; + direction: string; // TODO: modify this to the enum from ts-proto moving: boolean; } diff --git a/front/src/Connexion/EmoteEventStream.ts b/front/src/Connexion/EmoteEventStream.ts deleted file mode 100644 index 32f1daa0..00000000 --- a/front/src/Connexion/EmoteEventStream.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Subject } from "rxjs"; - -interface EmoteEvent { - userId: number; - emote: string; -} - -class EmoteEventStream { - private _stream: Subject<EmoteEvent> = new Subject(); - public stream = this._stream.asObservable(); - - fire(userId: number, emote: string) { - this._stream.next({ userId, emote }); - } -} - -export const emoteEventStream = new EmoteEventStream(); diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 4dce6924..cc84f043 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -22,8 +22,8 @@ const nonce = "nonce"; const notification = "notificationPermission"; const code = "code"; const cameraSetup = "cameraSetup"; - const cacheAPIIndex = "workavdenture-cache"; +const userProperties = "user-properties"; class LocalUserStore { saveUser(localUser: LocalUser) { @@ -136,13 +136,12 @@ class LocalUserStore { return localStorage.getItem(ignoreFollowRequests) === "true"; } - setLastRoomUrl(roomUrl: string): void { + async setLastRoomUrl(roomUrl: string): Promise<void> { localStorage.setItem(lastRoomUrl, roomUrl.toString()); if ("caches" in window) { - caches.open(cacheAPIIndex).then((cache) => { - const stringResponse = new Response(JSON.stringify({ roomUrl })); - cache.put(`/${lastRoomUrl}`, stringResponse); - }); + const cache = await caches.open(cacheAPIIndex); + const stringResponse = new Response(JSON.stringify({ roomUrl })); + await cache.put(`/${lastRoomUrl}`, stringResponse); } } getLastRoomUrl(): string { @@ -220,6 +219,27 @@ class LocalUserStore { const cameraSetupValues = localStorage.getItem(cameraSetup); return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; } + + getAllUserProperties(): Map<string, unknown> { + const result = new Map<string, string>(); + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key) { + if (key.startsWith(userProperties + "_")) { + const value = localStorage.getItem(key); + if (value) { + const userKey = key.substr((userProperties + "_").length); + result.set(userKey, JSON.parse(value)); + } + } + } + } + return result; + } + + setUserProperty(name: string, value: unknown): void { + localStorage.setItem(userProperties + "_" + name, JSON.stringify(value)); + } } export const localUserStore = new LocalUserStore(); diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index e5e2227f..74dd06de 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -18,7 +18,10 @@ export interface RoomRedirect { export class Room { public readonly id: string; - public readonly isPublic: boolean; + /** + * @deprecated + */ + private readonly isPublic: boolean; private _authenticationMandatory: boolean = DISABLE_ANONYMOUS; private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER; private _mapUrl: string | undefined; @@ -27,6 +30,8 @@ export class Room { private readonly _search: URLSearchParams; private _contactPage: string | undefined; private _group: string | null = null; + private _expireOn: Date | undefined; + private _canReport: boolean = false; private constructor(private roomUrl: URL) { this.id = roomUrl.pathname; @@ -34,7 +39,7 @@ export class Room { if (this.id.startsWith("/")) { this.id = this.id.substr(1); } - if (this.id.startsWith("_/")) { + if (this.id.startsWith("_/") || this.id.startsWith("*/")) { this.isPublic = true; } else if (this.id.startsWith("@/")) { this.isPublic = false; @@ -121,6 +126,10 @@ export class Room { data.authenticationMandatory != null ? data.authenticationMandatory : DISABLE_ANONYMOUS; this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER; this._contactPage = data.contactPage || CONTACT_URL; + if (data.expireOn) { + this._expireOn = new Date(data.expireOn); + } + this._canReport = data.canReport ?? false; return new MapDetail(data.mapUrl, data.textures); } else { throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format."); @@ -143,6 +152,8 @@ export class Room { * Instance name is: * - In a public URL: the second part of the URL ( _/[instance]/map.json) * - In a private URL: [organizationId/worldId] + * + * @deprecated */ public getInstance(): string { if (this.instance !== undefined) { @@ -150,7 +161,7 @@ export class Room { } if (this.isPublic) { - const match = /_\/([^/]+)\/.+/.exec(this.id); + const match = /[_*]\/([^/]+)\/.+/.exec(this.id); if (!match) throw new Error('Could not extract instance from "' + this.id + '"'); this.instance = match[1]; return this.instance; @@ -222,4 +233,12 @@ export class Room { get group(): string | null { return this._group; } + + get expireOn(): Date | undefined { + return this._expireOn; + } + + get canReport(): boolean { + return this._canReport; + } } diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 328f1aec..d360c705 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -1,50 +1,9 @@ import { PUSHER_URL, UPLOADER_URL } from "../Enum/EnvironmentVariable"; import Axios from "axios"; -import { - BatchMessage, - ClientToServerMessage, - GroupDeleteMessage, - GroupUpdateMessage, - ItemEventMessage, - PlayGlobalMessage, - PositionMessage, - RoomJoinedMessage, - ServerToClientMessage, - SetPlayerDetailsMessage, - SilentMessage, - StopGlobalMessage, - UserJoinedMessage, - UserLeftMessage, - UserMovedMessage, - UserMovesMessage, - ViewportMessage, - WebRtcDisconnectMessage, - WebRtcSignalToClientMessage, - WebRtcSignalToServerMessage, - WebRtcStartMessage, - ReportPlayerMessage, - TeleportMessageMessage, - QueryJitsiJwtMessage, - SendJitsiJwtMessage, - CharacterLayerMessage, - PingMessage, - EmoteEventMessage, - EmotePromptMessage, - FollowRequestMessage, - FollowConfirmationMessage, - FollowAbortMessage, - SendUserMessage, - BanUserMessage, - VariableMessage, - ErrorMessage, - PlayerDetailsUpdatedMessage, -} from "../Messages/generated/messages_pb"; import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; -import Direction = PositionMessage.Direction; import { ProtobufClientUtils } from "../Network/ProtobufClientUtils"; -import { - EventMessage, +import type { GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface, MessageUserJoined, @@ -59,13 +18,44 @@ import { } from "./ConnexionModels"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import { adminMessagesService } from "./AdminMessagesService"; -import { worldFullMessageStream } from "./WorldFullMessageStream"; import { connectionManager } from "./ConnectionManager"; -import { emoteEventStream } from "./EmoteEventStream"; import { get } from "svelte/store"; import { warningContainerStore } from "../Stores/MenuStore"; import { followStateStore, followRoleStore, followUsersStore } from "../Stores/FollowStore"; import { localUserStore } from "./LocalUserStore"; +import { + RefreshRoomMessage, + ServerToClientMessage as ServerToClientMessageTsProto, + TokenExpiredMessage, + WorldConnexionMessage, + WorldFullMessage, + ErrorMessage as ErrorMessageTsProto, + UserMovedMessage as UserMovedMessageTsProto, + GroupUpdateMessage as GroupUpdateMessageTsProto, + GroupDeleteMessage as GroupDeleteMessageTsProto, + UserJoinedMessage as UserJoinedMessageTsProto, + UserLeftMessage as UserLeftMessageTsProto, + ItemEventMessage as ItemEventMessageTsProto, + EmoteEventMessage as EmoteEventMessageTsProto, + VariableMessage as VariableMessageTsProto, + PlayerDetailsUpdatedMessage as PlayerDetailsUpdatedMessageTsProto, + WorldFullWarningMessage, + WebRtcDisconnectMessage as WebRtcDisconnectMessageTsProto, + PlayGlobalMessage as PlayGlobalMessageTsProto, + StopGlobalMessage as StopGlobalMessageTsProto, + SendJitsiJwtMessage as SendJitsiJwtMessageTsProto, + SendUserMessage as SendUserMessageTsProto, + BanUserMessage as BanUserMessageTsProto, + ClientToServerMessage as ClientToServerMessageTsProto, + PositionMessage as PositionMessageTsProto, + ViewportMessage as ViewportMessageTsProto, + PositionMessage_Direction, + SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto, + PingMessage as PingMessageTsProto, +} from "../Messages/ts-proto-generated/messages"; +import { Subject } from "rxjs"; +import { OpenPopupEvent } from "../Api/Events/OpenPopupEvent"; +import { match } from "assert"; const manualPingDelay = 20000; @@ -78,6 +68,79 @@ export class RoomConnection implements RoomConnection { private tags: string[] = []; private _userRoomToken: string | undefined; + private readonly _errorMessageStream = new Subject<ErrorMessageTsProto>(); + public readonly errorMessageStream = this._errorMessageStream.asObservable(); + + private readonly _roomJoinedMessageStream = new Subject<{ + connection: RoomConnection; + room: RoomJoinedMessageInterface; + }>(); + public readonly roomJoinedMessageStream = this._roomJoinedMessageStream.asObservable(); + + private readonly _webRtcStartMessageStream = new Subject<UserSimplePeerInterface>(); + public readonly webRtcStartMessageStream = this._webRtcStartMessageStream.asObservable(); + + private readonly _webRtcSignalToClientMessageStream = new Subject<WebRtcSignalReceivedMessageInterface>(); + public readonly webRtcSignalToClientMessageStream = this._webRtcSignalToClientMessageStream.asObservable(); + + private readonly _webRtcScreenSharingSignalToClientMessageStream = + new Subject<WebRtcSignalReceivedMessageInterface>(); + public readonly webRtcScreenSharingSignalToClientMessageStream = + this._webRtcScreenSharingSignalToClientMessageStream.asObservable(); + + private readonly _webRtcDisconnectMessageStream = new Subject<WebRtcDisconnectMessageTsProto>(); + public readonly webRtcDisconnectMessageStream = this._webRtcDisconnectMessageStream.asObservable(); + + private readonly _teleportMessageMessageStream = new Subject<string>(); + public readonly teleportMessageMessageStream = this._teleportMessageMessageStream.asObservable(); + + private readonly _sendJitsiJwtMessageStream = new Subject<SendJitsiJwtMessageTsProto>(); + public readonly sendJitsiJwtMessageStream = this._sendJitsiJwtMessageStream.asObservable(); + + private readonly _worldFullMessageStream = new Subject<string | null>(); + public readonly worldFullMessageStream = this._worldFullMessageStream.asObservable(); + + private readonly _worldConnexionMessageStream = new Subject<WorldConnexionMessage>(); + public readonly worldConnexionMessageStream = this._worldConnexionMessageStream.asObservable(); + + private readonly _tokenExpiredMessageStream = new Subject<TokenExpiredMessage>(); + public readonly tokenExpiredMessageStream = this._tokenExpiredMessageStream.asObservable(); + + private readonly _userMovedMessageStream = new Subject<UserMovedMessageTsProto>(); + public readonly userMovedMessageStream = this._userMovedMessageStream.asObservable(); + + private readonly _groupUpdateMessageStream = new Subject<GroupCreatedUpdatedMessageInterface>(); + public readonly groupUpdateMessageStream = this._groupUpdateMessageStream.asObservable(); + + private readonly _groupDeleteMessageStream = new Subject<GroupDeleteMessageTsProto>(); + public readonly groupDeleteMessageStream = this._groupDeleteMessageStream.asObservable(); + + private readonly _userJoinedMessageStream = new Subject<MessageUserJoined>(); + public readonly userJoinedMessageStream = this._userJoinedMessageStream.asObservable(); + + private readonly _userLeftMessageStream = new Subject<UserLeftMessageTsProto>(); + public readonly userLeftMessageStream = this._userLeftMessageStream.asObservable(); + + private readonly _itemEventMessageStream = new Subject<{ + itemId: number; + event: string; + parameters: unknown; + state: unknown; + }>(); + public readonly itemEventMessageStream = this._itemEventMessageStream.asObservable(); + + private readonly _emoteEventMessageStream = new Subject<EmoteEventMessageTsProto>(); + public readonly emoteEventMessageStream = this._emoteEventMessageStream.asObservable(); + + private readonly _variableMessageStream = new Subject<{ name: string; value: unknown }>(); + public readonly variableMessageStream = this._variableMessageStream.asObservable(); + + private readonly _playerDetailsUpdatedMessageStream = new Subject<PlayerDetailsUpdatedMessageTsProto>(); + public readonly playerDetailsUpdatedMessageStream = this._playerDetailsUpdatedMessageStream.asObservable(); + + private readonly _connectionErrorStream = new Subject<CloseEvent>(); + public readonly connectionErrorStream = this._connectionErrorStream.asObservable(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any public static setWebsocketFactory(websocketFactory: (url: string) => any): void { RoomConnection.websocketFactory = websocketFactory; @@ -136,8 +199,8 @@ export class RoomConnection implements RoomConnection { this.socket.onopen = (ev) => { //we manually ping every 20s to not be logged out by the server, even when the game is in background. - const pingMessage = new PingMessage(); - interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); + const pingMessage = PingMessageTsProto.encode({}).finish(); + interval = setInterval(() => this.socket.send(pingMessage), manualPingDelay); }; this.socket.addEventListener("close", (event) => { @@ -147,147 +210,252 @@ export class RoomConnection implements RoomConnection { // If we are not connected yet (if a JoinRoomMessage was not sent), we need to retry. if (this.userId === null && !this.closed) { - this.dispatch(EventMessage.CONNECTING_ERROR, event); + this._connectionErrorStream.next(event); } }); this.socket.onmessage = (messageEvent) => { const arrayBuffer: ArrayBuffer = messageEvent.data; - const message = ServerToClientMessage.deserializeBinary(new Uint8Array(arrayBuffer)); - if (message.hasBatchmessage()) { - for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) { - let event: string | null = null; - let payload; - if (subMessage.hasUsermovedmessage()) { - event = EventMessage.USER_MOVED; - payload = subMessage.getUsermovedmessage(); - } else if (subMessage.hasGroupupdatemessage()) { - event = EventMessage.GROUP_CREATE_UPDATE; - payload = subMessage.getGroupupdatemessage(); - } else if (subMessage.hasGroupdeletemessage()) { - event = EventMessage.GROUP_DELETE; - payload = subMessage.getGroupdeletemessage(); - } else if (subMessage.hasUserjoinedmessage()) { - event = EventMessage.JOIN_ROOM; - payload = subMessage.getUserjoinedmessage(); - } else if (subMessage.hasUserleftmessage()) { - event = EventMessage.USER_LEFT; - payload = subMessage.getUserleftmessage(); - } else if (subMessage.hasItemeventmessage()) { - event = EventMessage.ITEM_EVENT; - payload = subMessage.getItemeventmessage(); - } else if (subMessage.hasEmoteeventmessage()) { - const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage; - emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote()); - } else if (subMessage.hasPlayerdetailsupdatedmessage()) { - event = EventMessage.USER_DETAILS_UPDATED; - payload = subMessage.getPlayerdetailsupdatedmessage(); - } else if (subMessage.hasErrormessage()) { - const errorMessage = subMessage.getErrormessage() as ErrorMessage; - console.error("An error occurred server side: " + errorMessage.getMessage()); - } else if (subMessage.hasVariablemessage()) { - event = EventMessage.SET_VARIABLE; - payload = subMessage.getVariablemessage(); + const serverToClientMessage = ServerToClientMessageTsProto.decode(new Uint8Array(arrayBuffer)); + //const message = ServerToClientMessage.deserializeBinary(new Uint8Array(arrayBuffer)); + + const message = serverToClientMessage.message; + if (message === undefined) { + return; + } + + switch (message.$case) { + case "batchMessage": { + for (const subMessageWrapper of message.batchMessage.payload) { + const subMessage = subMessageWrapper.message; + if (subMessage === undefined) { + return; + } + switch (subMessage.$case) { + case "errorMessage": { + this._errorMessageStream.next(subMessage.errorMessage); + console.error("An error occurred server side: " + subMessage.errorMessage.message); + break; + } + case "userJoinedMessage": { + this._userJoinedMessageStream.next( + this.toMessageUserJoined(subMessage.userJoinedMessage) + ); + break; + } + case "userLeftMessage": { + this._userLeftMessageStream.next(subMessage.userLeftMessage); + break; + } + case "userMovedMessage": { + this._userMovedMessageStream.next(subMessage.userMovedMessage); + break; + } + case "groupUpdateMessage": { + this._groupUpdateMessageStream.next( + this.toGroupCreatedUpdatedMessage(subMessage.groupUpdateMessage) + ); + break; + } + case "groupDeleteMessage": { + this._groupDeleteMessageStream.next(subMessage.groupDeleteMessage); + break; + } + case "itemEventMessage": { + this._itemEventMessageStream.next({ + itemId: subMessage.itemEventMessage.itemId, + event: subMessage.itemEventMessage.event, + parameters: JSON.parse(subMessage.itemEventMessage.parametersJson), + state: JSON.parse(subMessage.itemEventMessage.stateJson), + }); + break; + } + case "emoteEventMessage": { + this._emoteEventMessageStream.next(subMessage.emoteEventMessage); + break; + } + case "playerDetailsUpdatedMessage": { + this._playerDetailsUpdatedMessageStream.next(subMessage.playerDetailsUpdatedMessage); + break; + } + case "variableMessage": { + const name = subMessage.variableMessage.name; + const serializedValue = subMessage.variableMessage.value; + let value: unknown = undefined; + if (serializedValue) { + try { + value = JSON.parse(serializedValue); + } catch (e) { + console.error( + 'Unable to unserialize value received from server for variable "' + + name + + '". Value received: "' + + serializedValue + + '". Error: ', + e + ); + } + } + + this._variableMessageStream.next({ name, value }); + break; + } + default: { + // Security check: if we forget a "case", the line below will catch the error at compile-time. + const tmp: never = subMessage; + } + } + } + break; + } + case "roomJoinedMessage": { + const roomJoinedMessage = message.roomJoinedMessage; + + const items: { [itemId: number]: unknown } = {}; + for (const item of roomJoinedMessage.item) { + items[item.itemId] = JSON.parse(item.stateJson); + } + + const variables = new Map<string, unknown>(); + for (const variable of roomJoinedMessage.variable) { + try { + variables.set(variable.name, JSON.parse(variable.value)); + } catch (e) { + console.error( + 'Unable to unserialize value received from server for variable "' + + variable.name + + '". Value received: "' + + variable.value + + '". Error: ', + e + ); + } + } + + this.userId = roomJoinedMessage.currentUserId; + this.tags = roomJoinedMessage.tag; + this._userRoomToken = roomJoinedMessage.userRoomToken; + + this._roomJoinedMessageStream.next({ + connection: this, + room: { + items, + variables, + } as RoomJoinedMessageInterface, + }); + break; + } + case "worldFullMessage": { + this._worldFullMessageStream.next(null); + this.closed = true; + break; + } + case "tokenExpiredMessage": { + connectionManager.logout().catch((e) => console.error(e)); + this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency + break; + } + case "worldConnexionMessage": { + this._worldFullMessageStream.next(message.worldConnexionMessage.message); + this.closed = true; + break; + } + case "webRtcSignalToClientMessage": { + this._webRtcSignalToClientMessageStream.next({ + userId: message.webRtcSignalToClientMessage.userId, + signal: JSON.parse(message.webRtcSignalToClientMessage.signal), + webRtcUser: message.webRtcSignalToClientMessage.webrtcUserName + ? message.webRtcSignalToClientMessage.webrtcUserName + : undefined, + webRtcPassword: message.webRtcSignalToClientMessage.webrtcPassword + ? message.webRtcSignalToClientMessage.webrtcPassword + : undefined, + }); + break; + } + case "webRtcScreenSharingSignalToClientMessage": { + this._webRtcScreenSharingSignalToClientMessageStream.next({ + userId: message.webRtcScreenSharingSignalToClientMessage.userId, + signal: JSON.parse(message.webRtcScreenSharingSignalToClientMessage.signal), + webRtcUser: message.webRtcScreenSharingSignalToClientMessage.webrtcUserName + ? message.webRtcScreenSharingSignalToClientMessage.webrtcUserName + : undefined, + webRtcPassword: message.webRtcScreenSharingSignalToClientMessage.webrtcPassword + ? message.webRtcScreenSharingSignalToClientMessage.webrtcPassword + : undefined, + }); + break; + } + case "webRtcStartMessage": { + this._webRtcStartMessageStream.next({ + userId: message.webRtcStartMessage.userId, + initiator: message.webRtcStartMessage.initiator, + webRtcUser: message.webRtcStartMessage.webrtcUserName + ? message.webRtcStartMessage.webrtcUserName + : undefined, + webRtcPassword: message.webRtcStartMessage.webrtcPassword + ? message.webRtcStartMessage.webrtcPassword + : undefined, + }); + break; + } + case "webRtcDisconnectMessage": { + this._webRtcDisconnectMessageStream.next(message.webRtcDisconnectMessage); + break; + } + case "teleportMessageMessage": { + // FIXME: WHY IS THIS UNUSED? CAN WE REMOVE THIS??? + this._teleportMessageMessageStream.next(message.teleportMessageMessage.map); + break; + } + case "sendJitsiJwtMessage": { + this._sendJitsiJwtMessageStream.next(message.sendJitsiJwtMessage); + break; + } + case "sendUserMessage": { + adminMessagesService.onSendusermessage(message.sendUserMessage); + break; + } + case "banUserMessage": { + adminMessagesService.onSendusermessage(message.banUserMessage); + break; + } + case "worldFullWarningMessage": { + warningContainerStore.activateWarningContainer(); + break; + } + case "refreshRoomMessage": { + //todo: implement a way to notify the user the room was refreshed. + break; + } + case "followRequestMessage": { + if (!localUserStore.getIgnoreFollowRequests()) { + followUsersStore.addFollowRequest(message.followRequestMessage.leader); + } + break; + } + case "followConfirmationMessage": { + followUsersStore.addFollower(message.followConfirmationMessage.follower); + break; + } + case "followAbortMessage": { + if (get(followRoleStore) === "follower") { + followUsersStore.stopFollowing(); } else { - throw new Error("Unexpected batch message type"); - } - - if (event) { - this.dispatch(event, payload); + followUsersStore.removeFollower(message.followAbortMessage.follower); } + break; } - } else if (message.hasRoomjoinedmessage()) { - const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage; - - const items: { [itemId: number]: unknown } = {}; - for (const item of roomJoinedMessage.getItemList()) { - items[item.getItemid()] = JSON.parse(item.getStatejson()); + case "errorMessage": { + this._errorMessageStream.next(message.errorMessage); + console.error("An error occurred server side: " + message.errorMessage.message); + break; } - - const variables = new Map<string, unknown>(); - for (const variable of roomJoinedMessage.getVariableList()) { - try { - variables.set(variable.getName(), JSON.parse(variable.getValue())); - } catch (e) { - console.error( - 'Unable to unserialize value received from server for variable "' + - variable.getName() + - '". Value received: "' + - variable.getValue() + - '". Error: ', - e - ); - } + default: { + // Security check: if we forget a "case", the line below will catch the error at compile-time. + const tmp: never = message; } - - this.userId = roomJoinedMessage.getCurrentuserid(); - this.tags = roomJoinedMessage.getTagList(); - this._userRoomToken = roomJoinedMessage.getUserroomtoken(); - - this.dispatch(EventMessage.CONNECT, { - connection: this, - room: { - items, - variables, - } as RoomJoinedMessageInterface, - }); - } else if (message.hasWorldfullmessage()) { - worldFullMessageStream.onMessage(); - this.closed = true; - } else if (message.hasTokenexpiredmessage()) { - connectionManager.logout(); - this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency - } 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()) { - this.dispatch( - EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, - message.getWebrtcscreensharingsignaltoclientmessage() - ); - } else if (message.hasWebrtcstartmessage()) { - this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage()); - } else if (message.hasWebrtcdisconnectmessage()) { - this.dispatch(EventMessage.WEBRTC_DISCONNECT, message.getWebrtcdisconnectmessage()); - } else if (message.hasPlayglobalmessage()) { - this.dispatch(EventMessage.PLAY_GLOBAL_MESSAGE, message.getPlayglobalmessage()); - } else if (message.hasStopglobalmessage()) { - this.dispatch(EventMessage.STOP_GLOBAL_MESSAGE, message.getStopglobalmessage()); - } else if (message.hasTeleportmessagemessage()) { - this.dispatch(EventMessage.TELEPORT, message.getTeleportmessagemessage()); - } else if (message.hasSendjitsijwtmessage()) { - this.dispatch(EventMessage.START_JITSI_ROOM, message.getSendjitsijwtmessage()); - } else if (message.hasSendusermessage()) { - adminMessagesService.onSendusermessage(message.getSendusermessage() as SendUserMessage); - } else if (message.hasBanusermessage()) { - adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage); - } else if (message.hasWorldfullwarningmessage()) { - warningContainerStore.activateWarningContainer(); - } else if (message.hasRefreshroommessage()) { - //todo: implement a way to notify the user the room was refreshed. - } else if (message.hasFollowrequestmessage()) { - const requestMessage = message.getFollowrequestmessage() as FollowRequestMessage; - if (!localUserStore.getIgnoreFollowRequests()) { - followUsersStore.addFollowRequest(requestMessage.getLeader()); - } - } else if (message.hasFollowconfirmationmessage()) { - const responseMessage = message.getFollowconfirmationmessage() as FollowConfirmationMessage; - followUsersStore.addFollower(responseMessage.getFollower()); - } else if (message.hasFollowabortmessage()) { - const abortMessage = message.getFollowabortmessage() as FollowAbortMessage; - if (get(followRoleStore) === "follower") { - followUsersStore.stopFollowing(); - } else { - followUsersStore.removeFollower(abortMessage.getFollower()); - } - } else if (message.hasErrormessage()) { - const errorMessage = message.getErrormessage() as ErrorMessage; - console.error("An error occurred server side: " + errorMessage.getMessage()); - } else { - throw new Error("Unknown message received"); } }; } @@ -314,17 +482,25 @@ export class RoomConnection implements RoomConnection { }*/ public emitPlayerOutlineColor(color: number | null) { - const message = new SetPlayerDetailsMessage(); + let message: SetPlayerDetailsMessageTsProto; if (color === null) { - message.setRemoveoutlinecolor(true); + message = SetPlayerDetailsMessageTsProto.fromPartial({ + removeOutlineColor: true, + }); } else { - message.setOutlinecolor(color); + message = SetPlayerDetailsMessageTsProto.fromPartial({ + outlineColor: color, + }); } - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setSetplayerdetailsmessage(message); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "setPlayerDetailsMessage", + setPlayerDetailsMessage: message, + }, + }).finish(); - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } public closeConnection(): void { @@ -332,41 +508,35 @@ export class RoomConnection implements RoomConnection { this.closed = true; } - private toPositionMessage(x: number, y: number, direction: string, moving: boolean): PositionMessage { - const positionMessage = new PositionMessage(); - positionMessage.setX(Math.floor(x)); - positionMessage.setY(Math.floor(y)); - let directionEnum: Direction; - switch (direction) { - case "up": - directionEnum = Direction.UP; - break; - case "down": - directionEnum = Direction.DOWN; - break; - case "left": - directionEnum = Direction.LEFT; - break; - case "right": - directionEnum = Direction.RIGHT; - break; - default: - throw new Error("Unexpected direction"); - } - positionMessage.setDirection(directionEnum); - positionMessage.setMoving(moving); - - return positionMessage; + private toPositionMessage(x: number, y: number, direction: string, moving: boolean): PositionMessageTsProto { + return { + x: Math.floor(x), + y: Math.floor(y), + moving, + direction: (() => { + switch (direction) { + case "up": + return PositionMessage_Direction.UP; + case "down": + return PositionMessage_Direction.DOWN; + case "left": + return PositionMessage_Direction.LEFT; + case "right": + return PositionMessage_Direction.RIGHT; + default: + throw new Error("Unexpected direction"); + } + })(), + }; } - private toViewportMessage(viewport: ViewportInterface): ViewportMessage { - const viewportMessage = new ViewportMessage(); - viewportMessage.setLeft(Math.floor(viewport.left)); - viewportMessage.setRight(Math.floor(viewport.right)); - viewportMessage.setTop(Math.floor(viewport.top)); - viewportMessage.setBottom(Math.floor(viewport.bottom)); - - return viewportMessage; + private toViewportMessage(viewport: ViewportInterface): ViewportMessageTsProto { + return { + left: Math.floor(viewport.left), + right: Math.floor(viewport.right), + top: Math.floor(viewport.top), + bottom: Math.floor(viewport.bottom), + }; } public sharePosition(x: number, y: number, direction: string, moving: boolean, viewport: ViewportInterface): void { @@ -378,81 +548,77 @@ export class RoomConnection implements RoomConnection { const viewportMessage = this.toViewportMessage(viewport); - const userMovesMessage = new UserMovesMessage(); - userMovesMessage.setPosition(positionMessage); - userMovesMessage.setViewport(viewportMessage); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "userMovesMessage", + userMovesMessage: { + position: positionMessage, + viewport: viewportMessage, + }, + }, + }).finish(); - //console.log('Sending position ', positionMessage.getX(), positionMessage.getY()); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setUsermovesmessage(userMovesMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } public setSilent(silent: boolean): void { - const silentMessage = new SilentMessage(); - silentMessage.setSilent(silent); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "silentMessage", + silentMessage: { + silent, + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setSilentmessage(silentMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } public setViewport(viewport: ViewportInterface): void { - const viewportMessage = new ViewportMessage(); - viewportMessage.setTop(Math.round(viewport.top)); - viewportMessage.setBottom(Math.round(viewport.bottom)); - viewportMessage.setLeft(Math.round(viewport.left)); - viewportMessage.setRight(Math.round(viewport.right)); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "viewportMessage", + viewportMessage: this.toViewportMessage(viewport), + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setViewportmessage(viewportMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } - public onUserJoins(callback: (message: MessageUserJoined) => void): void { + /* public onUserJoins(callback: (message: MessageUserJoined) => void): void { this.onMessage(EventMessage.JOIN_ROOM, (message: UserJoinedMessage) => { callback(this.toMessageUserJoined(message)); }); - } + }*/ // TODO: move this to protobuf utils - private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined { - const position = message.getPosition(); + private toMessageUserJoined(message: UserJoinedMessageTsProto): MessageUserJoined { + const position = message.position; if (position === undefined) { throw new Error("Invalid JOIN_ROOM message"); } - const characterLayers = message - .getCharacterlayersList() - .map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => { - return { - name: characterLayer.getName(), - img: characterLayer.getUrl(), - }; - }); + const characterLayers = message.characterLayers.map((characterLayer): BodyResourceDescriptionInterface => { + return { + name: characterLayer.name, + img: characterLayer.url, + }; + }); - const companion = message.getCompanion(); + const companion = message.companion; return { - userId: message.getUserid(), - name: message.getName(), + userId: message.userId, + name: message.name, characterLayers, - visitCardUrl: message.getVisitcardurl(), + visitCardUrl: message.visitCardUrl, position: ProtobufClientUtils.toPointInterface(position), - companion: companion ? companion.getName() : null, - userUuid: message.getUseruuid(), - outlineColor: message.getHasoutline() ? message.getOutlinecolor() : undefined, + companion: companion ? companion.name : null, + userUuid: message.userUuid, + outlineColor: message.hasOutline ? message.outlineColor : undefined, }; } - public onUserMoved(callback: (message: UserMovedMessage) => void): void { - this.onMessage(EventMessage.USER_MOVED, callback); - //this.socket.on(EventMessage.USER_MOVED, callback); - } - /** * Registers a listener on a message that is part of a batch */ @@ -465,114 +631,49 @@ export class RoomConnection implements RoomConnection { callbacks.push(callback); } - public onUserLeft(callback: (userId: number) => void): void { - this.onMessage(EventMessage.USER_LEFT, (message: UserLeftMessage) => { - callback(message.getUserid()); - }); - } - - public onGroupUpdatedOrCreated( - callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void - ): void { - this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => { - callback(this.toGroupCreatedUpdatedMessage(message)); - }); - } - - private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface { - const position = message.getPosition(); + private toGroupCreatedUpdatedMessage(message: GroupUpdateMessageTsProto): GroupCreatedUpdatedMessageInterface { + const position = message.position; if (position === undefined) { throw new Error("Missing position in GROUP_CREATE_UPDATE"); } return { - groupId: message.getGroupid(), - position: position.toObject(), - groupSize: message.getGroupsize(), + groupId: message.groupId, + position: position, + groupSize: message.groupSize, }; } - public onGroupDeleted(callback: (groupId: number) => void): void { - this.onMessage(EventMessage.GROUP_DELETE, (message: GroupDeleteMessage) => { - callback(message.getGroupid()); - }); - } - - public onConnectingError(callback: (event: CloseEvent) => void): void { - this.onMessage(EventMessage.CONNECTING_ERROR, (event: CloseEvent) => { - callback(event); - }); - } - public onConnectError(callback: (error: Event) => void): void { this.socket.addEventListener("error", callback); } - public onConnect(callback: (roomConnection: OnConnectInterface) => void): void { - //this.socket.addEventListener('open', callback) - this.onMessage(EventMessage.CONNECT, callback); - } - - /** - * Triggered when we receive all the details of a room (users, groups, ...) - */ - /*public onStartRoom(callback: (event: RoomJoinedMessageInterface) => void): void { - this.onMessage(EventMessage.START_ROOM, callback); - }*/ - public sendWebrtcSignal(signal: unknown, receiverId: number) { - const webRtcSignal = new WebRtcSignalToServerMessage(); - webRtcSignal.setReceiverid(receiverId); - webRtcSignal.setSignal(JSON.stringify(signal)); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "webRtcSignalToServerMessage", + webRtcSignalToServerMessage: { + receiverId, + signal: JSON.stringify(signal), + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setWebrtcsignaltoservermessage(webRtcSignal); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } public sendWebrtcScreenSharingSignal(signal: unknown, receiverId: number) { - const webRtcSignal = new WebRtcSignalToServerMessage(); - webRtcSignal.setReceiverid(receiverId); - webRtcSignal.setSignal(JSON.stringify(signal)); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "webRtcScreenSharingSignalToServerMessage", + webRtcScreenSharingSignalToServerMessage: { + receiverId, + signal: JSON.stringify(signal), + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setWebrtcscreensharingsignaltoservermessage(webRtcSignal); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); - } - - public receiveWebrtcStart(callback: (message: UserSimplePeerInterface) => void) { - this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => { - callback({ - userId: message.getUserid(), - initiator: message.getInitiator(), - webRtcUser: message.getWebrtcusername() ?? undefined, - webRtcPassword: message.getWebrtcpassword() ?? undefined, - }); - }); - } - - public receiveWebrtcSignal(callback: (message: WebRtcSignalReceivedMessageInterface) => void) { - this.onMessage(EventMessage.WEBRTC_SIGNAL, (message: WebRtcSignalToClientMessage) => { - callback({ - userId: message.getUserid(), - signal: JSON.parse(message.getSignal()), - webRtcUser: message.getWebrtcusername() ?? undefined, - webRtcPassword: message.getWebrtcpassword() ?? undefined, - }); - }); - } - - public receiveWebrtcScreenSharingSignal(callback: (message: WebRtcSignalReceivedMessageInterface) => void) { - this.onMessage(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, (message: WebRtcSignalToClientMessage) => { - callback({ - userId: message.getUserid(), - signal: JSON.parse(message.getSignal()), - webRtcUser: message.getWebrtcusername() ?? undefined, - webRtcPassword: message.getWebrtcpassword() ?? undefined, - }); - }); + this.socket.send(bytes); } public onServerDisconnected(callback: () => void): void { @@ -594,61 +695,34 @@ export class RoomConnection implements RoomConnection { return this.userId; } - disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void { - this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => { - callback({ - userId: message.getUserid(), - }); - }); - } - emitActionableEvent(itemId: number, event: string, state: unknown, parameters: unknown): void { - const itemEventMessage = new ItemEventMessage(); - itemEventMessage.setItemid(itemId); - itemEventMessage.setEvent(event); - itemEventMessage.setStatejson(JSON.stringify(state)); - itemEventMessage.setParametersjson(JSON.stringify(parameters)); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "itemEventMessage", + itemEventMessage: { + itemId, + event, + stateJson: JSON.stringify(state), + parametersJson: JSON.stringify(parameters), + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setItemeventmessage(itemEventMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } emitSetVariableEvent(name: string, value: unknown): void { - const variableMessage = new VariableMessage(); - variableMessage.setName(name); - variableMessage.setValue(JSON.stringify(value)); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "variableMessage", + variableMessage: { + name, + value: JSON.stringify(value), + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setVariablemessage(variableMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); - } - - onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void { - this.onMessage(EventMessage.ITEM_EVENT, (message: ItemEventMessage) => { - callback({ - itemId: message.getItemid(), - event: message.getEvent(), - parameters: JSON.parse(message.getParametersjson()), - state: JSON.parse(message.getStatejson()), - }); - }); - } - - onPlayerDetailsUpdated(callback: (message: PlayerDetailsUpdatedMessageInterface) => void): void { - this.onMessage(EventMessage.USER_DETAILS_UPDATED, (message: PlayerDetailsUpdatedMessage) => { - const details = message.getDetails(); - if (details === undefined) { - throw new Error("Malformed message. Missing details in PlayerDetailsUpdatedMessage"); - } - callback({ - userId: message.getUserid(), - outlineColor: details.getOutlinecolor(), - removeOutlineColor: details.getRemoveoutlinecolor(), - }); - }); + this.socket.send(bytes); } public uploadAudio(file: FormData) { @@ -662,91 +736,48 @@ export class RoomConnection implements RoomConnection { }); } - /* public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) { - return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => { - callback({ - id: message.getId(), - type: message.getType(), - message: message.getMessage(), - }); - }); - }*/ - - public receiveStopGlobalMessage(callback: (messageId: string) => void) { - return this.onMessage(EventMessage.STOP_GLOBAL_MESSAGE, (message: StopGlobalMessage) => { - callback(message.getId()); - }); - } - - public receiveTeleportMessage(callback: (messageId: string) => void) { - return this.onMessage(EventMessage.TELEPORT, (message: TeleportMessageMessage) => { - callback(message.getMap()); - }); - } - public emitGlobalMessage(message: PlayGlobalMessageInterface): void { - const playGlobalMessage = new PlayGlobalMessage(); - playGlobalMessage.setType(message.type); - playGlobalMessage.setContent(message.content); - playGlobalMessage.setBroadcasttoworld(message.broadcastToWorld); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "playGlobalMessage", + playGlobalMessage: { + type: message.type, + content: message.content, + broadcastToWorld: message.broadcastToWorld, + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setPlayglobalmessage(playGlobalMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } public emitReportPlayerMessage(reportedUserUuid: string, reportComment: string): void { - const reportPlayerMessage = new ReportPlayerMessage(); - reportPlayerMessage.setReporteduseruuid(reportedUserUuid); - reportPlayerMessage.setReportcomment(reportComment); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "reportPlayerMessage", + reportPlayerMessage: { + reportedUserUuid, + reportComment, + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setReportplayermessage(reportPlayerMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string | undefined): void { - const queryJitsiJwtMessage = new QueryJitsiJwtMessage(); - queryJitsiJwtMessage.setJitsiroom(jitsiRoom); - if (tag !== undefined) { - queryJitsiJwtMessage.setTag(tag); - } + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "queryJitsiJwtMessage", + queryJitsiJwtMessage: { + jitsiRoom, + tag: tag ?? "", // empty string is sent as "undefined" by ts-proto + // TODO: when we migrated "pusher" to ts-proto, migrate this to a StringValue + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setQueryjitsijwtmessage(queryJitsiJwtMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); - } - - public onStartJitsiRoom(callback: (jwt: string, room: string) => void): void { - this.onMessage(EventMessage.START_JITSI_ROOM, (message: SendJitsiJwtMessage) => { - callback(message.getJwt(), message.getJitsiroom()); - }); - } - - public onSetVariable(callback: (name: string, value: unknown) => void): void { - this.onMessage(EventMessage.SET_VARIABLE, (message: VariableMessage) => { - const name = message.getName(); - const serializedValue = message.getValue(); - let value: unknown = undefined; - if (serializedValue) { - try { - value = JSON.parse(serializedValue); - } catch (e) { - console.error( - 'Unable to unserialize value received from server for variable "' + - name + - '". Value received: "' + - serializedValue + - '". Error: ', - e - ); - } - } - callback(name, value); - }); + this.socket.send(bytes); } public hasTag(tag: string): boolean { @@ -758,36 +789,51 @@ export class RoomConnection implements RoomConnection { } public emitEmoteEvent(emoteName: string): void { - const emoteMessage = new EmotePromptMessage(); - emoteMessage.setEmote(emoteName); + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "emotePromptMessage", + emotePromptMessage: { + emote: emoteName, + }, + }, + }).finish(); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setEmotepromptmessage(emoteMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); + this.socket.send(bytes); } public emitFollowRequest(): void { if (!this.userId) { return; } - const message = new FollowRequestMessage(); - message.setLeader(this.userId); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setFollowrequestmessage(message); - this.socket.send(clientToServerMessage.serializeBinary().buffer); + + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "followRequestMessage", + followRequestMessage: { + leader: this.userId, + }, + }, + }).finish(); + + this.socket.send(bytes); } public emitFollowConfirmation(): void { if (!this.userId) { return; } - const message = new FollowConfirmationMessage(); - message.setLeader(get(followUsersStore)[0]); - message.setFollower(this.userId); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setFollowconfirmationmessage(message); - this.socket.send(clientToServerMessage.serializeBinary().buffer); + + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "followConfirmationMessage", + followConfirmationMessage: { + leader: get(followUsersStore)[0], + follower: this.userId, + }, + }, + }).finish(); + + this.socket.send(bytes); } public emitFollowAbort(): void { @@ -796,12 +842,18 @@ export class RoomConnection implements RoomConnection { if (!this.userId || (isLeader && !hasFollowers)) { return; } - const message = new FollowAbortMessage(); - message.setLeader(isLeader ? this.userId : get(followUsersStore)[0]); - message.setFollower(isLeader ? 0 : this.userId); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setFollowabortmessage(message); - this.socket.send(clientToServerMessage.serializeBinary().buffer); + + const bytes = ClientToServerMessageTsProto.encode({ + message: { + $case: "followAbortMessage", + followAbortMessage: { + leader: isLeader ? this.userId : get(followUsersStore)[0], + follower: isLeader ? 0 : this.userId, + }, + }, + }).finish(); + + this.socket.send(bytes); } public getAllTags(): string[] { diff --git a/front/src/Connexion/WorldFullMessageStream.ts b/front/src/Connexion/WorldFullMessageStream.ts deleted file mode 100644 index 01ce6f20..00000000 --- a/front/src/Connexion/WorldFullMessageStream.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Subject } from "rxjs"; - -class WorldFullMessageStream { - private _stream: Subject<string | null> = new Subject<string | null>(); - public stream = this._stream.asObservable(); - - onMessage(message?: string) { - this._stream.next(message); - } -} - -export const worldFullMessageStream = new WorldFullMessageStream(); diff --git a/front/src/Messages/.gitignore b/front/src/Messages/.gitignore deleted file mode 100644 index 9e0adcc1..00000000 --- a/front/src/Messages/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/generated/ diff --git a/front/src/Messages/ts-proto-generated/.gitignore b/front/src/Messages/ts-proto-generated/.gitignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/front/src/Messages/ts-proto-generated/.gitignore @@ -0,0 +1 @@ +* diff --git a/front/src/Network/ProtobufClientUtils.ts b/front/src/Network/ProtobufClientUtils.ts index 9ba0f40b..3e172d0f 100644 --- a/front/src/Network/ProtobufClientUtils.ts +++ b/front/src/Network/ProtobufClientUtils.ts @@ -1,21 +1,21 @@ -import { PositionMessage } from "../Messages/generated/messages_pb"; -import Direction = PositionMessage.Direction; +import { PositionMessage, PositionMessage_Direction } from "../Messages/ts-proto-generated/messages"; + import type { PointInterface } from "../Connexion/ConnexionModels"; export class ProtobufClientUtils { public static toPointInterface(position: PositionMessage): PointInterface { let direction: string; - switch (position.getDirection()) { - case Direction.UP: + switch (position.direction) { + case PositionMessage_Direction.UP: direction = "up"; break; - case Direction.DOWN: + case PositionMessage_Direction.DOWN: direction = "down"; break; - case Direction.LEFT: + case PositionMessage_Direction.LEFT: direction = "left"; break; - case Direction.RIGHT: + case PositionMessage_Direction.RIGHT: direction = "right"; break; default: @@ -24,10 +24,10 @@ export class ProtobufClientUtils { // sending to all clients in room except sender return { - x: position.getX(), - y: position.getY(), + x: position.x, + y: position.y, direction, - moving: position.getMoving(), + moving: position.moving, }; } } diff --git a/front/src/Phaser/Companion/Companion.ts b/front/src/Phaser/Companion/Companion.ts index 80b0236e..6157ebaa 100644 --- a/front/src/Phaser/Companion/Companion.ts +++ b/front/src/Phaser/Companion/Companion.ts @@ -41,13 +41,15 @@ export class Companion extends Container { this.companionName = name; this._pictureStore = writable(undefined); - texturePromise.then((resource) => { - this.addResource(resource); - this.invisible = false; - return this.getSnapshot().then((htmlImageElementSrc) => { - this._pictureStore.set(htmlImageElementSrc); - }); - }); + texturePromise + .then((resource) => { + this.addResource(resource); + this.invisible = false; + return this.getSnapshot().then((htmlImageElementSrc) => { + this._pictureStore.set(htmlImageElementSrc); + }); + }) + .catch((e) => console.error(e)); this.scene.physics.world.enableBody(this); diff --git a/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts b/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts index bd87ba75..98cceafa 100644 --- a/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts +++ b/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts @@ -3,7 +3,7 @@ import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./Co export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => { COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => { - lazyLoadCompanionResource(loader, resource.name); + lazyLoadCompanionResource(loader, resource.name).catch((e) => console.error(e)); }); return COMPANION_RESOURCES; diff --git a/front/src/Phaser/Components/Loader.ts b/front/src/Phaser/Components/Loader.ts index e013e758..7eb08e6d 100644 --- a/front/src/Phaser/Components/Loader.ts +++ b/front/src/Phaser/Components/Loader.ts @@ -72,9 +72,11 @@ export class Loader { if (this.loadingText) { this.loadingText.destroy(); } - promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => { - resLoadingImage.destroy(); - }); + promiseLoadLogoTexture + .then((resLoadingImage: Phaser.GameObjects.Image) => { + resLoadingImage.destroy(); + }) + .catch((e) => console.error(e)); this.progress.destroy(); this.progressContainer.destroy(); if (this.scene instanceof DirtyScene) { diff --git a/front/src/Phaser/Game/EmbeddedWebsiteManager.ts b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts index bf9f14e4..387940c7 100644 --- a/front/src/Phaser/Game/EmbeddedWebsiteManager.ts +++ b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts @@ -16,7 +16,8 @@ export class EmbeddedWebsiteManager { if (website === undefined) { throw new Error('Cannot find embedded website with name "' + name + '"'); } - const rect = website.iframe.getBoundingClientRect(); + + const scale = website.scale ?? 1; return { url: website.url, name: website.name, @@ -26,9 +27,11 @@ export class EmbeddedWebsiteManager { position: { x: website.phaserObject.x, y: website.phaserObject.y, - width: rect["width"], - height: rect["height"], + width: website.phaserObject.width * scale, + height: website.phaserObject.height * scale, }, + origin: website.origin, + scale: website.scale, }; }); @@ -59,7 +62,9 @@ export class EmbeddedWebsiteManager { createEmbeddedWebsiteEvent.position.height, createEmbeddedWebsiteEvent.visible ?? true, createEmbeddedWebsiteEvent.allowApi ?? false, - createEmbeddedWebsiteEvent.allow ?? "" + createEmbeddedWebsiteEvent.allow ?? "", + createEmbeddedWebsiteEvent.origin ?? "map", + createEmbeddedWebsiteEvent.scale ?? 1 ); } ); @@ -107,10 +112,18 @@ export class EmbeddedWebsiteManager { website.phaserObject.y = embeddedWebsiteEvent.y; } if (embeddedWebsiteEvent?.width !== undefined) { - website.iframe.style.width = embeddedWebsiteEvent.width + "px"; + website.position.width = embeddedWebsiteEvent.width; + website.iframe.style.width = embeddedWebsiteEvent.width / website.phaserObject.scale + "px"; } if (embeddedWebsiteEvent?.height !== undefined) { - website.iframe.style.height = embeddedWebsiteEvent.height + "px"; + website.position.height = embeddedWebsiteEvent.height; + website.iframe.style.height = embeddedWebsiteEvent.height / website.phaserObject.scale + "px"; + } + + if (embeddedWebsiteEvent?.scale !== undefined) { + website.phaserObject.scale = embeddedWebsiteEvent.scale; + website.iframe.style.width = website.position.width / embeddedWebsiteEvent.scale + "px"; + website.iframe.style.height = website.position.height / embeddedWebsiteEvent.scale + "px"; } } ); @@ -125,7 +138,9 @@ export class EmbeddedWebsiteManager { height: number, visible: boolean, allowApi: boolean, - allow: string + allow: string, + origin: "map" | "player" | undefined, + scale: number | undefined ): void { if (this.embeddedWebsites.has(name)) { throw new Error('An embedded website with the name "' + name + '" already exists in your map'); @@ -135,9 +150,9 @@ export class EmbeddedWebsiteManager { name, url, /*x, - y, - width, - height,*/ +y, +width, +height,*/ allow, allowApi, visible, @@ -147,6 +162,8 @@ export class EmbeddedWebsiteManager { width, height, }, + origin, + scale, }; const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible); @@ -161,22 +178,43 @@ export class EmbeddedWebsiteManager { const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString(); const iframe = document.createElement("iframe"); + const scale = embeddedWebsiteEvent.scale ?? 1; + iframe.src = absoluteUrl; iframe.tabIndex = -1; - iframe.style.width = embeddedWebsiteEvent.position.width + "px"; - iframe.style.height = embeddedWebsiteEvent.position.height + "px"; + iframe.style.width = embeddedWebsiteEvent.position.width / scale + "px"; + iframe.style.height = embeddedWebsiteEvent.position.height / scale + "px"; iframe.style.margin = "0"; iframe.style.padding = "0"; iframe.style.border = "none"; + const domElement = new DOMElement( + this.gameScene, + embeddedWebsiteEvent.position.x, + embeddedWebsiteEvent.position.y, + iframe + ); + domElement.setOrigin(0, 0); + if (embeddedWebsiteEvent.scale) { + domElement.scale = embeddedWebsiteEvent.scale; + } + domElement.setVisible(visible); + + switch (embeddedWebsiteEvent.origin) { + case "player": + this.gameScene.CurrentPlayer.add(domElement); + break; + case "map": + default: + this.gameScene.add.existing(domElement); + } + const embeddedWebsite = { ...embeddedWebsiteEvent, - phaserObject: this.gameScene.add - .dom(embeddedWebsiteEvent.position.x, embeddedWebsiteEvent.position.y, iframe) - .setVisible(visible) - .setOrigin(0, 0), + phaserObject: domElement, iframe: iframe, }; + if (embeddedWebsiteEvent.allowApi) { iframeListener.registerIframe(iframe); } diff --git a/front/src/Phaser/Game/EmoteManager.ts b/front/src/Phaser/Game/EmoteManager.ts index 06e8b099..097ebf45 100644 --- a/front/src/Phaser/Game/EmoteManager.ts +++ b/front/src/Phaser/Game/EmoteManager.ts @@ -1,13 +1,13 @@ -import { emoteEventStream } from "../../Connexion/EmoteEventStream"; import type { GameScene } from "./GameScene"; import type { Subscription } from "rxjs"; +import type { RoomConnection } from "../../Connexion/RoomConnection"; export class EmoteManager { private subscription: Subscription; - constructor(private scene: GameScene) { - this.subscription = emoteEventStream.stream.subscribe((event) => { - const actor = this.scene.MapPlayersByKey.get(event.userId); + constructor(private scene: GameScene, private connection: RoomConnection) { + this.subscription = connection.emoteEventMessageStream.subscribe((event) => { + const actor = this.scene.MapPlayersByKey.get(event.actorUserId); if (actor) { actor.playEmote(event.emote); } diff --git a/front/src/Phaser/Game/GameMapPropertiesListener.ts b/front/src/Phaser/Game/GameMapPropertiesListener.ts index 2dc36df8..f6c28862 100644 --- a/front/src/Phaser/Game/GameMapPropertiesListener.ts +++ b/front/src/Phaser/Game/GameMapPropertiesListener.ts @@ -123,7 +123,7 @@ export class GameMapPropertiesListener { .then((coWebsite) => { const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer); if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) { - coWebsiteManager.closeCoWebsite(coWebsite); + coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => console.error(e)); this.coWebsitesOpenByLayer.delete(layer); this.coWebsitesActionTriggerByLayer.delete(layer); } else { @@ -132,7 +132,8 @@ export class GameMapPropertiesListener { state: OpenCoWebsiteState.OPENED, }); } - }); + }) + .catch((e) => console.error(e)); layoutManagerActionStore.removeAction(actionUuid); }; @@ -198,7 +199,7 @@ export class GameMapPropertiesListener { } if (coWebsiteOpen.coWebsite !== undefined) { - coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite); + coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite).catch((e) => console.error(e)); } this.coWebsitesOpenByLayer.delete(layer); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 4800e259..b6d7274c 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -40,7 +40,6 @@ import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; import { GameMap } from "./GameMap"; import { PlayerMovement } from "./PlayerMovement"; import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; -import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; import { DirtyScene } from "./DirtyScene"; import { TextUtils } from "../Components/TextUtils"; import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; @@ -60,7 +59,6 @@ import type { PositionInterface, RoomJoinedMessageInterface, } from "../../Connexion/ConnexionModels"; -import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; import type { RoomConnection } from "../../Connexion/RoomConnection"; import type { ActionableItem } from "../Items/ActionableItem"; import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; @@ -90,9 +88,10 @@ import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile; import { deepCopy } from "deep-copy-ts"; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import { MapStore } from "../../Stores/Utils/MapStore"; -import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb"; import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore"; import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator"; +import Camera = Phaser.Cameras.Scene2D.Camera; +import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -210,6 +209,8 @@ export class GameScene extends DirtyScene { private objectsByType = new Map<string, ITiledMapObject[]>(); private embeddedWebsiteManager!: EmbeddedWebsiteManager; private loader: Loader; + private lastCameraEvent: WasCameraUpdatedEvent | undefined; + private firstCameraUpdateSent: boolean = false; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -240,7 +241,7 @@ export class GameScene extends DirtyScene { const textures = localUser?.textures; if (textures) { for (const texture of textures) { - loadCustomTexture(this.load, texture); + loadCustomTexture(this.load, texture).catch((e) => console.error(e)); } } @@ -267,7 +268,7 @@ export class GameScene extends DirtyScene { this.load.on( "filecomplete-tilemapJSON-" + this.MapUrlFile, (key: string, type: string, data: unknown) => { - this.onMapLoad(data); + this.onMapLoad(data).catch((e) => console.error(e)); } ); return; @@ -291,14 +292,14 @@ export class GameScene extends DirtyScene { this.load.on( "filecomplete-tilemapJSON-" + this.MapUrlFile, (key: string, type: string, data: unknown) => { - this.onMapLoad(data); + this.onMapLoad(data).catch((e) => console.error(e)); } ); // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered. // In this case, we check in the cache to see if the map is here and trigger the event manually. if (this.cache.tilemap.exists(this.MapUrlFile)) { const data = this.cache.tilemap.get(this.MapUrlFile); - this.onMapLoad(data); + this.onMapLoad(data).catch((e) => console.error(e)); } return; } @@ -319,7 +320,7 @@ export class GameScene extends DirtyScene { }); this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles"); this.load.on("filecomplete-tilemapJSON-" + this.MapUrlFile, (key: string, type: string, data: unknown) => { - this.onMapLoad(data); + this.onMapLoad(data).catch((e) => console.error(e)); }); //TODO strategy to add access token this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); @@ -327,7 +328,7 @@ export class GameScene extends DirtyScene { // In this case, we check in the cache to see if the map is here and trigger the event manually. if (this.cache.tilemap.exists(this.MapUrlFile)) { const data = this.cache.tilemap.get(this.MapUrlFile); - this.onMapLoad(data); + this.onMapLoad(data).catch((e) => console.error(e)); } //eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -405,21 +406,23 @@ export class GameScene extends DirtyScene { this.load.on("complete", () => { // FIXME: the factory might fail because the resources might not be loaded yet... // We would need to add a loader ended event in addition to the createPromise - this.createPromise.then(async () => { - itemFactory.create(this); + this.createPromise + .then(async () => { + itemFactory.create(this); - const roomJoinedAnswer = await this.connectionAnswerPromise; + const roomJoinedAnswer = await this.connectionAnswerPromise; - for (const object of objectsOfType) { - // TODO: we should pass here a factory to create sprites (maybe?) + for (const object of objectsOfType) { + // TODO: we should pass here a factory to create sprites (maybe?) - // Do we have a state for this object? - const state = roomJoinedAnswer.items[object.id]; + // Do we have a state for this object? + const state = roomJoinedAnswer.items[object.id]; - const actionableItem = itemFactory.factory(this, object, state); - this.actionableItems.set(actionableItem.getId(), actionableItem); - } - }); + const actionableItem = itemFactory.factory(this, object, state); + this.actionableItems.set(actionableItem.getId(), actionableItem); + } + }) + .catch((e) => console.error(e)); }); } } @@ -448,10 +451,6 @@ export class GameScene extends DirtyScene { this.pinchManager = new PinchManager(this); } - this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => - this.showWorldFullError(message) - ); - const playerName = gameManager.getPlayerName(); if (!playerName) { throw "playerName is not set"; @@ -489,11 +488,11 @@ export class GameScene extends DirtyScene { if (exitSceneUrl !== undefined) { this.loadNextGame( Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile) - ); + ).catch((e) => console.error(e)); } const exitUrl = this.getExitUrl(layer); if (exitUrl !== undefined) { - this.loadNextGameFromExitUrl(exitUrl); + this.loadNextGameFromExitUrl(exitUrl).catch((e) => console.error(e)); } } if (layer.type === "objectgroup") { @@ -523,7 +522,9 @@ export class GameScene extends DirtyScene { object.height, object.visible, allowApi ?? false, - "" + "", + "map", + 1 ); } } @@ -531,7 +532,7 @@ export class GameScene extends DirtyScene { } this.gameMap.exitUrls.forEach((exitUrl) => { - this.loadNextGameFromExitUrl(exitUrl); + this.loadNextGameFromExitUrl(exitUrl).catch((e) => console.error(e)); }); this.startPositionCalculator = new StartPositionCalculator( @@ -552,7 +553,10 @@ export class GameScene extends DirtyScene { mediaManager.setUserInputManager(this.userInputManager); if (localUserStore.getFullscreen()) { - document.querySelector("body")?.requestFullscreen(); + document + .querySelector("body") + ?.requestFullscreen() + .catch((e) => console.error(e)); } //notify game manager can to create currentUser in map @@ -617,8 +621,6 @@ export class GameScene extends DirtyScene { this.connect(); } - this.emoteManager = new EmoteManager(this); - let oldPeerNumber = 0; this.peerStoreUnsubscribe = peerStore.subscribe((peers) => { const newPeerNumber = peers.size; @@ -660,9 +662,16 @@ export class GameScene extends DirtyScene { } }); - Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises]).then(() => { - this.scene.wake(); - }); + Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises]) + .then(() => { + this.scene.wake(); + }) + .catch((e) => + console.error( + "Some scripts failed to load ot the connection failed to establish to WorkAdventure server", + e + ) + ); } /** @@ -693,7 +702,7 @@ export class GameScene extends DirtyScene { playersStore.connectToRoomConnection(this.connection); userIsAdminStore.set(this.connection.hasTag("admin")); - this.connection.onUserJoins((message: MessageUserJoined) => { + this.connection.userJoinedMessageStream.subscribe((message) => { const userMessage: AddPlayerInterface = { userId: message.userId, characterLayers: message.characterLayers, @@ -707,31 +716,33 @@ export class GameScene extends DirtyScene { this.addPlayer(userMessage); }); - this.connection.onUserMoved((message: UserMovedMessage) => { - const position = message.getPosition(); + this.connection.userMovedMessageStream.subscribe((message) => { + const position = message.position; if (position === undefined) { throw new Error("Position missing from UserMovedMessage"); } const messageUserMoved: MessageUserMovedInterface = { - userId: message.getUserid(), + userId: message.userId, position: ProtobufClientUtils.toPointInterface(position), }; this.updatePlayerPosition(messageUserMoved); }); - this.connection.onUserLeft((userId: number) => { - this.removePlayer(userId); + this.connection.userLeftMessageStream.subscribe((message) => { + this.removePlayer(message.userId); }); - this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { - this.shareGroupPosition(groupPositionMessage); - }); + this.connection.groupUpdateMessageStream.subscribe( + (groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { + this.shareGroupPosition(groupPositionMessage); + } + ); - this.connection.onGroupDeleted((groupId: number) => { + this.connection.groupDeleteMessageStream.subscribe((message) => { try { - this.deleteGroup(groupId); + this.deleteGroup(message.groupId); } catch (e) { console.error(e); } @@ -743,7 +754,7 @@ export class GameScene extends DirtyScene { this.createSuccessorGameScene(true, true); }); - this.connection.onActionableEvent((message) => { + this.connection.itemEventMessageStream.subscribe((message) => { const item = this.actionableItems.get(message.itemId); if (item === undefined) { console.warn( @@ -756,18 +767,29 @@ export class GameScene extends DirtyScene { item.fire(message.event, message.state, message.parameters); }); - this.connection.onPlayerDetailsUpdated((message) => { + this.connection.playerDetailsUpdatedMessageStream.subscribe((message) => { + if (message.details === undefined) { + throw new Error("Malformed message. Missing details in PlayerDetailsUpdatedMessage"); + } this.pendingEvents.enqueue({ type: "PlayerDetailsUpdated", - details: message, + details: { + userId: message.userId, + outlineColor: message.details.outlineColor, + removeOutlineColor: message.details.removeOutlineColor, + }, }); }); /** * Triggered when we receive the JWT token to connect to Jitsi */ - this.connection.onStartJitsiRoom((jwt, room) => { - this.startJitsi(room, jwt); + this.connection.sendJitsiJwtMessageStream.subscribe((message) => { + this.startJitsi(message.jitsiRoom, message.jwt); + }); + + this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => { + this.showWorldFullError(message); }); // When connection is performed, let's connect SimplePeer @@ -842,12 +864,15 @@ export class GameScene extends DirtyScene { }); }); + this.emoteManager = new EmoteManager(this, this.connection); + // this.gameMap.onLeaveLayer((layers) => { // layers.forEach((layer) => { // iframeListener.sendLeaveLayerEvent(layer.name); // }); // }); - }); + }) + .catch((e) => console.error(e)); } //todo: into dedicated classes @@ -900,7 +925,7 @@ export class GameScene extends DirtyScene { if (newValue) { this.onMapExit( Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile) - ); + ).catch((e) => console.error(e)); } else { setTimeout(() => { layoutManagerActionStore.removeAction("roomAccessDenied"); @@ -909,7 +934,9 @@ export class GameScene extends DirtyScene { }); this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => { if (newValue) { - this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())); + this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())).catch((e) => + console.error(e) + ); } else { setTimeout(() => { layoutManagerActionStore.removeAction("roomAccessDenied"); @@ -1095,21 +1122,47 @@ ${escapedMessage} 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); + soundManager + .playSound(this.load, this.sound, url.toString(), playSoundEvent.config) + .catch((e) => console.error(e)); }) ); this.iframeSubscriptionList.push( - iframeListener.stopSoundStream.subscribe((stopSoundEvent) => { - const url = new URL(stopSoundEvent.url, this.MapUrlFile); - soundManager.stopSound(this.sound, url.toString()); + iframeListener.trackCameraUpdateStream.subscribe(() => { + if (!this.firstCameraUpdateSent) { + this.cameras.main.on("followupdate", (camera: Camera) => { + const cameraEvent: WasCameraUpdatedEvent = { + x: camera.worldView.x, + y: camera.worldView.y, + width: camera.worldView.width, + height: camera.worldView.height, + zoom: camera.scaleManager.zoom, + }; + if ( + this.lastCameraEvent?.x == cameraEvent.x && + this.lastCameraEvent?.y == cameraEvent.y && + this.lastCameraEvent?.width == cameraEvent.width && + this.lastCameraEvent?.height == cameraEvent.height && + this.lastCameraEvent?.zoom == cameraEvent.zoom + ) { + return; + } + + this.lastCameraEvent = cameraEvent; + iframeListener.sendCameraUpdated(cameraEvent); + this.firstCameraUpdateSent = true; + }); + + iframeListener.sendCameraUpdated(this.cameras.main); + } }) ); 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()).catch((e) => console.error(e)); }) ); @@ -1120,11 +1173,15 @@ ${escapedMessage} ); this.iframeSubscriptionList.push( iframeListener.loadPageStream.subscribe((url: string) => { - this.loadNextGameFromExitUrl(url).then(() => { - this.events.once(EVENT_TYPE.POST_UPDATE, () => { - this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString())); - }); - }); + this.loadNextGameFromExitUrl(url) + .then(() => { + this.events.once(EVENT_TYPE.POST_UPDATE, () => { + this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString())).catch((e) => + console.error(e) + ); + }); + }) + .catch((e) => console.error(e)); }) ); let scriptedBubbleSprite: Sprite; @@ -1165,6 +1222,12 @@ ${escapedMessage} }) ); + this.iframeSubscriptionList.push( + iframeListener.setPropertyStream.subscribe((setProperty) => { + this.setPropertyLayer(setProperty.layerName, setProperty.propertyName, setProperty.propertyValue); + }) + ); + iframeListener.registerAnswerer("openCoWebsite", async (openCoWebsite, source) => { if (!source) { throw new Error("Unknown query source"); @@ -1235,6 +1298,7 @@ ${escapedMessage} roomId: this.roomUrl, tags: this.connection ? this.connection.getAllTags() : [], variables: this.sharedVariablesManager.variables, + playerVariables: localUserStore.getAllUserProperties(), userRoomToken: this.connection ? this.connection.userRoomToken : "", }; }); @@ -1325,6 +1389,22 @@ ${escapedMessage} }) ); + iframeListener.registerAnswerer("setVariable", (event, source) => { + switch (event.target) { + case "global": { + this.sharedVariablesManager.setVariable(event, source); + break; + } + case "player": { + localUserStore.setUserProperty(event.key, event.value); + break; + } + default: { + const _exhaustiveCheck: never = event.target; + } + } + }); + iframeListener.registerAnswerer("removeActionMessage", (message) => { layoutManagerActionStore.removeAction(message.uuid); }); @@ -1343,6 +1423,13 @@ ${escapedMessage} this.CurrentPlayer.removeOutlineColor(); this.connection?.emitPlayerOutlineColor(null); }); + + iframeListener.registerAnswerer("getPlayerPosition", () => { + return { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y, + }; + }); } private setPropertyLayer( @@ -1351,7 +1438,7 @@ ${escapedMessage} propertyValue: string | number | boolean | undefined ): void { if (propertyName === GameMapProperties.EXIT_URL && typeof propertyValue === "string") { - this.loadNextGameFromExitUrl(propertyValue); + this.loadNextGameFromExitUrl(propertyValue).catch((e) => console.error(e)); } this.gameMap.setLayerProperty(layerName, propertyName, propertyValue); } @@ -1436,7 +1523,7 @@ ${escapedMessage} public cleanupClosingScene(): void { // stop playing audio, close any open website, stop any open Jitsi - coWebsiteManager.closeCoWebsites(); + coWebsiteManager.closeCoWebsites().catch((e) => console.error(e)); // Stop the script, if any const scripts = this.getScriptUrls(this.mapFile); for (const script of scripts) { @@ -1467,6 +1554,7 @@ ${escapedMessage} iframeListener.unregisterAnswerer("openCoWebsite"); iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("setPlayerOutline"); + iframeListener.unregisterAnswerer("setVariable"); this.sharedVariablesManager?.close(); this.embeddedWebsiteManager?.close(); @@ -1945,6 +2033,7 @@ ${escapedMessage} this.loader.resize(); } + private getObjectLayerData(objectName: string): ITiledMapObject | undefined { for (const layer of this.mapFile.layers) { if (layer.type === "objectgroup" && layer.name === "floorLayer") { @@ -1957,6 +2046,7 @@ ${escapedMessage} } return undefined; } + private reposition(): void { // Recompute camera offset if needed biggestAvailableAreaStore.recompute(); @@ -1975,7 +2065,9 @@ ${escapedMessage} const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; const jitsiWidth = allProps.get(GameMapProperties.JITSI_WIDTH) as number | undefined; - jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth); + jitsiFactory + .start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth) + .catch((e) => console.error(e)); this.connection?.setSilent(true); mediaManager.hideGameOverlay(); analyticsClient.enteredJitsi(roomName, this.room.id); diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index 5b5867dc..3c019e74 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -3,6 +3,7 @@ import { iframeListener } from "../../Api/IframeListener"; import type { GameMap } from "./GameMap"; import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap"; import { GameMapProperties } from "./GameMapProperties"; +import type { SetVariableEvent } from "../../Api/Events/SetVariableEvent"; interface Variable { defaultValue: unknown; @@ -41,58 +42,58 @@ export class SharedVariablesManager { this._variables.set(name, value); } - roomConnection.onSetVariable((name, value) => { + roomConnection.variableMessageStream.subscribe(({ name, value }) => { this._variables.set(name, value); // On server change, let's notify the iframes iframeListener.setVariable({ key: name, value: value, + target: "global", }); }); + } - // When a variable is modified from an iFrame - iframeListener.registerAnswerer("setVariable", (event, source) => { - const key = event.key; + public setVariable(event: SetVariableEvent, source: MessageEventSource | null): void { + const key = event.key; - const object = this.variableObjects.get(key); + const object = this.variableObjects.get(key); - if (object === undefined) { - const errMsg = - 'A script is trying to modify variable "' + - key + - '" but this variable is not defined in the map.' + - 'There should be an object in the map whose name is "' + - key + - '" and whose type is "variable"'; - console.error(errMsg); - throw new Error(errMsg); - } + if (object === undefined) { + const errMsg = + 'A script is trying to modify variable "' + + key + + '" but this variable is not defined in the map.' + + 'There should be an object in the map whose name is "' + + key + + '" and whose type is "variable"'; + console.error(errMsg); + throw new Error(errMsg); + } - if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) { - const errMsg = - 'A script is trying to modify variable "' + - key + - '" but this variable is only writable for users with tag "' + - object.writableBy + - '".'; - console.error(errMsg); - throw new Error(errMsg); - } + if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) { + const errMsg = + 'A script is trying to modify variable "' + + key + + '" but this variable is only writable for users with tag "' + + object.writableBy + + '".'; + console.error(errMsg); + throw new Error(errMsg); + } - // Let's stop any propagation of the value we set is the same as the existing value. - if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) { - return; - } + // Let's stop any propagation of the value we set is the same as the existing value. + if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) { + return; + } - this._variables.set(key, event.value); + this._variables.set(key, event.value); - // Dispatch to the room connection. - this.roomConnection.emitSetVariableEvent(key, event.value); + // Dispatch to the room connection. + this.roomConnection.emitSetVariableEvent(key, event.value); - // Dispatch to other iframes - iframeListener.dispatchVariableToOtherIframes(key, event.value, source); - }); + // Dispatch to other iframes + iframeListener.dispatchVariableToOtherIframes(key, event.value, source); } private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> { diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts index d5629c88..5c208edd 100644 --- a/front/src/Phaser/Login/CustomizeScene.ts +++ b/front/src/Phaser/Login/CustomizeScene.ts @@ -40,19 +40,21 @@ export class CustomizeScene extends AbstractCharacterScene { } preload() { - this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => { - bodyResourceDescriptions.forEach((bodyResourceDescription) => { - if ( - bodyResourceDescription.level == undefined || - bodyResourceDescription.level < 0 || - bodyResourceDescription.level > 5 - ) { - throw "Texture level is null"; - } - this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription); - }); - this.lazyloadingAttempt = true; - }); + this.loadCustomSceneSelectCharacters() + .then((bodyResourceDescriptions) => { + bodyResourceDescriptions.forEach((bodyResourceDescription) => { + if ( + bodyResourceDescription.level == undefined || + bodyResourceDescription.level < 0 || + bodyResourceDescription.level > 5 + ) { + throw "Texture level is null"; + } + this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription); + }); + this.lazyloadingAttempt = true; + }) + .catch((e) => console.error(e)); this.layers = loadAllLayers(this.load); this.lazyloadingAttempt = false; diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index 64fa9791..4e372e0e 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -41,12 +41,14 @@ export class SelectCharacterScene extends AbstractCharacterScene { } preload() { - this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => { - bodyResourceDescriptions.forEach((bodyResourceDescription) => { - this.playerModels.push(bodyResourceDescription); - }); - this.lazyloadingAttempt = true; - }); + this.loadSelectSceneCharacters() + .then((bodyResourceDescriptions) => { + bodyResourceDescriptions.forEach((bodyResourceDescription) => { + this.playerModels.push(bodyResourceDescription); + }); + this.lazyloadingAttempt = true; + }) + .catch((e) => console.error(e)); this.playerModels = loadAllDefaultModels(this.load); this.lazyloadingAttempt = false; diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index 57bb13c9..74810df4 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -162,6 +162,7 @@ export interface ITiledTileSet { imageheight: number; imagewidth: number; + columns: number; margin: number; name: string; properties?: ITiledMapProperty[]; diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 946bb6c4..e41b3237 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -100,10 +100,6 @@ export class Player extends Character { return [xMovement, yMovement]; } - public enableFollowing() { - followStateStore.set("active"); - } - public moveUser(delta: number): void { const activeEvents = this.userInputManager.getEventListForGameTick(); const state = get(followStateStore); @@ -111,8 +107,7 @@ export class Player extends Character { if (activeEvents.get(UserInputEvent.Follow)) { if (state === "off" && this.scene.groups.size > 0) { - followStateStore.set("requesting"); - followRoleStore.set("leader"); + this.sendFollowRequest(); } else if (state === "active") { followStateStore.set("ending"); } @@ -125,4 +120,15 @@ export class Player extends Character { } this.inputStep(activeEvents, x, y); } + + public sendFollowRequest() { + this.scene.connection?.emitFollowRequest(); + followRoleStore.set("leader"); + followStateStore.set("active"); + } + + public startFollowing() { + followStateStore.set("active"); + this.scene.connection?.emitFollowConfirmation(); + } } diff --git a/front/src/Phaser/Services/WaScaleManager.ts b/front/src/Phaser/Services/WaScaleManager.ts index 447b6a1f..c0c01402 100644 --- a/front/src/Phaser/Services/WaScaleManager.ts +++ b/front/src/Phaser/Services/WaScaleManager.ts @@ -31,6 +31,10 @@ export class WaScaleManager { height: height * devicePixelRatio, }); + if (gameSize.width == 0) { + return; + } + this.actualZoom = realSize.width / gameSize.width / devicePixelRatio; this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio); diff --git a/front/src/Stores/FollowStore.ts b/front/src/Stores/FollowStore.ts index ab1e61d1..eb9753d9 100644 --- a/front/src/Stores/FollowStore.ts +++ b/front/src/Stores/FollowStore.ts @@ -58,7 +58,6 @@ export const followUsersStore = createFollowUsersStore(); export const followUsersColorStore = derived( [followStateStore, followRoleStore, followUsersStore], ([$followStateStore, $followRoleStore, $followUsersStore]) => { - console.log($followStateStore); if ($followStateStore !== "active") { return undefined; } diff --git a/front/src/Stores/GameStore.ts b/front/src/Stores/GameStore.ts index eada6d26..e5298b7c 100644 --- a/front/src/Stores/GameStore.ts +++ b/front/src/Stores/GameStore.ts @@ -5,3 +5,5 @@ export const userMovingStore = writable(false); export const requestVisitCardsStore = writable<string | null>(null); export const userIsAdminStore = writable(false); + +export const limitMapStore = writable(false); diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index a0f1a92b..7eeac35d 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -360,32 +360,27 @@ const implementCorrectTrackBehavior = getNavigatorType() === NavigatorType.firef /** * Stops the camera from filming */ -function applyCameraConstraints(currentStream: MediaStream | null, constraints: MediaTrackConstraints | boolean): void { +async function applyCameraConstraints( + currentStream: MediaStream | null, + constraints: MediaTrackConstraints | boolean +): Promise<void[]> { if (!currentStream) { - return; - } - for (const track of currentStream.getVideoTracks()) { - toggleConstraints(track, constraints).catch((e) => - console.error("Error while setting new camera constraints:", e) - ); + return []; } + return Promise.all(currentStream.getVideoTracks().map((track) => toggleConstraints(track, constraints))); } /** * Stops the microphone from listening */ -function applyMicrophoneConstraints( +async function applyMicrophoneConstraints( currentStream: MediaStream | null, constraints: MediaTrackConstraints | boolean -): void { +): Promise<void[]> { if (!currentStream) { - return; - } - for (const track of currentStream.getAudioTracks()) { - toggleConstraints(track, constraints).catch((e) => - console.error("Error while setting new audio constraints:", e) - ); + return []; } + return Promise.all(currentStream.getAudioTracks().map((track) => toggleConstraints(track, constraints))); } async function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrackConstraints | boolean): Promise<void> { @@ -477,8 +472,8 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS } } - applyMicrophoneConstraints(currentStream, constraints.audio || false); - applyCameraConstraints(currentStream, constraints.video || false); + applyMicrophoneConstraints(currentStream, constraints.audio || false).catch((e) => console.error(e)); + applyCameraConstraints(currentStream, constraints.video || false).catch((e) => console.error(e)); if (implementCorrectTrackBehavior) { //on good navigators like firefox, we can instantiate the stream once and simply disable or enable the tracks as needed diff --git a/front/src/Stores/ModalStore.ts b/front/src/Stores/ModalStore.ts new file mode 100644 index 00000000..3e99cde9 --- /dev/null +++ b/front/src/Stores/ModalStore.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; + +export const showLimitRoomModalStore = writable(false); +export const showShareLinkMapModalStore = writable(false); diff --git a/front/src/Stores/PlayersStore.ts b/front/src/Stores/PlayersStore.ts index 07c18b96..0676235a 100644 --- a/front/src/Stores/PlayersStore.ts +++ b/front/src/Stores/PlayersStore.ts @@ -3,6 +3,7 @@ import type { PlayerInterface } from "../Phaser/Game/PlayerInterface"; import type { RoomConnection } from "../Connexion/RoomConnection"; import { getRandomColor } from "../WebRtc/ColorGenerator"; import { localUserStore } from "../Connexion/LocalUserStore"; +import room from "../Api/iframe/room"; let idCount = 0; @@ -19,7 +20,8 @@ function createPlayersStore() { connectToRoomConnection: (roomConnection: RoomConnection) => { players = new Map<number, PlayerInterface>(); set(players); - roomConnection.onUserJoins((message) => { + // TODO: it would be cool to unsubscribe properly here + roomConnection.userJoinedMessageStream.subscribe((message) => { update((users) => { users.set(message.userId, { userId: message.userId, @@ -33,9 +35,9 @@ function createPlayersStore() { return users; }); }); - roomConnection.onUserLeft((userId) => { + roomConnection.userLeftMessageStream.subscribe((message) => { update((users) => { - users.delete(userId); + users.delete(message.userId); return users; }); }); diff --git a/front/src/Stores/ScreenSharingStore.ts b/front/src/Stores/ScreenSharingStore.ts index d68dbf8b..dc2c495c 100644 --- a/front/src/Stores/ScreenSharingStore.ts +++ b/front/src/Stores/ScreenSharingStore.ts @@ -156,7 +156,7 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra error: e instanceof Error ? e : new Error("An unknown error happened"), }); } - })(); + })().catch((e) => console.error(e)); } ); diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts index 50dbedc9..011efa5a 100644 --- a/front/src/Url/UrlManager.ts +++ b/front/src/Url/UrlManager.ts @@ -2,8 +2,7 @@ import type { Room } from "../Connexion/Room"; import { localUserStore } from "../Connexion/LocalUserStore"; export enum GameConnexionTypes { - anonymous = 1, - organization, + room = 1, register, empty, unknown, @@ -19,10 +18,8 @@ class UrlManager { return GameConnexionTypes.login; } else if (url === "/jwt") { return GameConnexionTypes.jwt; - } else if (url.includes("_/")) { - return GameConnexionTypes.anonymous; - } else if (url.includes("@/")) { - return GameConnexionTypes.organization; + } else if (url.includes("_/") || url.includes("*/") || url.includes("@/")) { + return GameConnexionTypes.room; } else if (url.includes("register/")) { return GameConnexionTypes.register; } else if (url === "/") { @@ -41,7 +38,7 @@ class UrlManager { if (window.location.pathname === room.id) return; //Set last room visited! (connected or nor, must to be saved in localstorage and cache API) //use href to keep # value - localUserStore.setLastRoomUrl(room.href); + localUserStore.setLastRoomUrl(room.href).catch((e) => console.error(e)); const hash = window.location.hash; const search = room.search.toString(); history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash); diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 7a003604..8bff2acb 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -149,7 +149,7 @@ class CoWebsiteManager { } buttonCloseCoWebsites.blur(); - this.closeCoWebsites(); + this.closeCoWebsites().catch((e) => console.error(e)); }); const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId); @@ -515,70 +515,72 @@ class CoWebsiteManager { throw new Error("Too many we"); } - Promise.resolve(callback(this.cowebsiteBufferDom)).then((iframe) => { - iframe?.classList.add("pixel"); + Promise.resolve(callback(this.cowebsiteBufferDom)) + .then((iframe) => { + iframe?.classList.add("pixel"); - if (!iframe.id) { - do { - iframe.id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7); - } while (this.getCoWebsiteById(iframe.id)); - } - - const onloadPromise = new Promise<void>((resolve) => { - iframe.onload = () => resolve(); - }); - - const icon = this.generateCoWebsiteIcon(iframe); - - const coWebsite = { - iframe, - icon, - position: position ?? this.coWebsites.length, - }; - - // Iframe management on mobile - icon.addEventListener("click", () => { - if (this.isSmallScreen()) { - this.moveRightPreviousCoWebsite(coWebsite, 0); + if (!iframe.id) { + do { + iframe.id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7); + } while (this.getCoWebsiteById(iframe.id)); } - }); - this.coWebsites.push(coWebsite); - this.cowebsiteSubIconsDom.appendChild(icon); + const onloadPromise = new Promise<void>((resolve) => { + iframe.onload = () => resolve(); + }); - const onTimeoutPromise = new Promise<void>((resolve) => { - setTimeout(() => resolve(), 2000); - }); + const icon = this.generateCoWebsiteIcon(iframe); - this.currentOperationPromise = this.currentOperationPromise - .then(() => Promise.race([onloadPromise, onTimeoutPromise])) - .then(() => { - if (coWebsite.position === 0) { - this.openMain(); - if (widthPercent) { - this.widthPercent = widthPercent; - } + const coWebsite = { + iframe, + icon, + position: position ?? this.coWebsites.length, + }; - setTimeout(() => { - this.fire(); + // Iframe management on mobile + icon.addEventListener("click", () => { + if (this.isSmallScreen()) { + this.moveRightPreviousCoWebsite(coWebsite, 0); + } + }); + + this.coWebsites.push(coWebsite); + this.cowebsiteSubIconsDom.appendChild(icon); + + const onTimeoutPromise = new Promise<void>((resolve) => { + setTimeout(() => resolve(), 2000); + }); + + this.currentOperationPromise = this.currentOperationPromise + .then(() => Promise.race([onloadPromise, onTimeoutPromise])) + .then(() => { + if (coWebsite.position === 0) { + this.openMain(); + if (widthPercent) { + this.widthPercent = widthPercent; + } + + setTimeout(() => { + this.fire(); + position !== undefined + ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position) + : this.moveCoWebsite(coWebsite, coWebsite.position); + }, animationTime); + } else { position !== undefined ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position) : this.moveCoWebsite(coWebsite, coWebsite.position); - }, animationTime); - } else { - position !== undefined - ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position) - : this.moveCoWebsite(coWebsite, coWebsite.position); - } + } - return resolve(coWebsite); - }) - .catch((err) => { - console.error("Error loadCoWebsite => ", err); - this.removeCoWebsiteFromStack(coWebsite); - return reject(); - }); - }); + return resolve(coWebsite); + }) + .catch((err) => { + console.error("Error loadCoWebsite => ", err); + this.removeCoWebsiteFromStack(coWebsite); + return reject(); + }); + }) + .catch((e) => console.error("Error loadCoWebsite >=> ", e)); }); } @@ -603,17 +605,21 @@ class CoWebsiteManager { return this.currentOperationPromise; } - public closeJitsi() { + public async closeJitsi() { const jitsi = this.searchJitsi(); if (jitsi) { - this.closeCoWebsite(jitsi); + return this.closeCoWebsite(jitsi); } } public closeCoWebsites(): Promise<void> { this.currentOperationPromise = this.currentOperationPromise.then(() => { + const promises: Promise<void>[] = []; this.coWebsites.forEach((coWebsite: CoWebsite) => { - this.closeCoWebsite(coWebsite); + promises.push(this.closeCoWebsite(coWebsite)); + }); + return Promise.all(promises).then(() => { + return; }); }); return this.currentOperationPromise; diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index 0f205f47..c067a255 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -1,5 +1,5 @@ import { JITSI_URL } from "../Enum/EnvironmentVariable"; -import { coWebsiteManager } from "./CoWebsiteManager"; +import { CoWebsite, coWebsiteManager } from "./CoWebsiteManager"; import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore"; import { get } from "svelte/store"; @@ -140,8 +140,8 @@ class JitsiFactory { interfaceConfig?: object, jitsiUrl?: string, jitsiWidth?: number - ): void { - coWebsiteManager.addCoWebsite( + ): Promise<CoWebsite> { + return coWebsiteManager.addCoWebsite( async (cowebsiteDiv) => { // Jitsi meet external API maintains some data in local storage // which is sent via the appData URL parameter when joining a @@ -200,7 +200,7 @@ class JitsiFactory { const jitsiCoWebsite = coWebsiteManager.searchJitsi(); if (jitsiCoWebsite) { - coWebsiteManager.closeJitsi(); + coWebsiteManager.closeJitsi().catch((e) => console.error(e)); } this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index ccbd0012..f4016015 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -75,23 +75,25 @@ export class SimplePeer { */ private initialise() { //receive signal by gemer - this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => { + this.Connection.webRtcSignalToClientMessageStream.subscribe((message: WebRtcSignalReceivedMessageInterface) => { this.receiveWebrtcSignal(message); }); //receive signal by gemer - this.Connection.receiveWebrtcScreenSharingSignal((message: WebRtcSignalReceivedMessageInterface) => { - this.receiveWebrtcScreenSharingSignal(message); - }); + this.Connection.webRtcScreenSharingSignalToClientMessageStream.subscribe( + (message: WebRtcSignalReceivedMessageInterface) => { + this.receiveWebrtcScreenSharingSignal(message); + } + ); mediaManager.showGameOverlay(); //receive message start - this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => { + this.Connection.webRtcStartMessageStream.subscribe((message: UserSimplePeerInterface) => { this.receiveWebrtcStart(message); }); - this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => { + this.Connection.webRtcDisconnectMessageStream.subscribe((data: WebRtcDisconnectMessageInterface): void => { this.closeConnection(data.userId); }); } diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 93415b0d..66ee77c0 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,30 +9,34 @@ import { } from "./Api/Events/IframeEvent"; import chat from "./Api/iframe/chat"; import type { IframeCallback } from "./Api/iframe/IframeApiContribution"; -import nav from "./Api/iframe/nav"; +import nav, { CoWebsite } from "./Api/iframe/nav"; import controls from "./Api/iframe/controls"; import ui from "./Api/iframe/ui"; import sound from "./Api/iframe/sound"; import room, { setMapURL, setRoomId } from "./Api/iframe/room"; -import state, { initVariables } from "./Api/iframe/state"; +import { createState } from "./Api/iframe/state"; import player, { setPlayerName, setTags, setUserRoomToken, setUuid } 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"; import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution"; +import camera from "./Api/iframe/camera"; + +const globalState = createState("global"); // Notify WorkAdventure that we are ready to receive data const initPromise = queryWorkadventure({ type: "getState", data: undefined, -}).then((state) => { - setPlayerName(state.nickname); - setRoomId(state.roomId); - setMapURL(state.mapUrl); - setTags(state.tags); - setUuid(state.uuid); - initVariables(state.variables as Map<string, unknown>); - setUserRoomToken(state.userRoomToken); +}).then((gameState) => { + setPlayerName(gameState.nickname); + setRoomId(gameState.roomId); + setMapURL(gameState.mapUrl); + setTags(gameState.tags); + setUuid(gameState.uuid); + globalState.initVariables(gameState.variables as Map<string, unknown>); + player.state.initVariables(gameState.playerVariables as Map<string, unknown>); + setUserRoomToken(gameState.userRoomToken); }); const wa = { @@ -43,7 +47,8 @@ const wa = { sound, room, player, - state, + camera, + state: globalState, onInit(): Promise<void> { return initPromise; @@ -131,17 +136,17 @@ const wa = { /** * @deprecated Use WA.nav.openCoWebSite instead */ - openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void { + openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): Promise<CoWebsite> { console.warn("Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead"); - nav.openCoWebSite(url, allowApi, allowPolicy); + return nav.openCoWebSite(url, allowApi, allowPolicy); }, /** * @deprecated Use WA.nav.closeCoWebSite instead */ - closeCoWebSite(): void { + closeCoWebSite(): Promise<void> { console.warn("Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead"); - nav.closeCoWebSite(); + return nav.closeCoWebSite(); }, /** @@ -225,7 +230,5 @@ window.addEventListener( callback?.callback(payloadData); } } - - // ... } ); diff --git a/front/style/style.scss b/front/style/style.scss index 89437a99..32d95f57 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -1066,6 +1066,7 @@ div.action.danger p.action-body{ width: 100%; height: 100%; pointer-events: none; + user-select: none; & > div { position: relative; diff --git a/front/tests/Phaser/Game/PlayerMovementTest.ts b/front/tests/Phaser/Game/PlayerMovementTest.ts index bd5f40b4..95b84f0b 100644 --- a/front/tests/Phaser/Game/PlayerMovementTest.ts +++ b/front/tests/Phaser/Game/PlayerMovementTest.ts @@ -1,22 +1,24 @@ import "jasmine"; -import {PlayerMovement} from "../../../src/Phaser/Game/PlayerMovement"; +import { PlayerMovement } from "../../../src/Phaser/Game/PlayerMovement"; describe("Interpolation / Extrapolation", () => { it("should interpolate", () => { - const playerMovement = new PlayerMovement({ - x: 100, y: 200 - }, 42000, + const playerMovement = new PlayerMovement( + { + x: 100, + y: 200, + }, + 42000, { x: 200, y: 100, oldX: 100, oldY: 200, moving: true, - direction: "up" + direction: "up", }, 42200 - ); - + ); expect(playerMovement.isOutdated(42100)).toBe(false); expect(playerMovement.isOutdated(43000)).toBe(true); @@ -26,8 +28,8 @@ describe("Interpolation / Extrapolation", () => { y: 150, oldX: 100, oldY: 200, - direction: 'up', - moving: true + direction: "up", + moving: true, }); expect(playerMovement.getPosition(42200)).toEqual({ @@ -35,8 +37,8 @@ describe("Interpolation / Extrapolation", () => { y: 100, oldX: 100, oldY: 200, - direction: 'up', - moving: true + direction: "up", + moving: true, }); expect(playerMovement.getPosition(42300)).toEqual({ @@ -44,22 +46,25 @@ describe("Interpolation / Extrapolation", () => { y: 50, oldX: 100, oldY: 200, - direction: 'up', - moving: true + direction: "up", + moving: true, }); }); it("should not extrapolate if we stop", () => { - const playerMovement = new PlayerMovement({ - x: 100, y: 200 - }, 42000, + const playerMovement = new PlayerMovement( + { + x: 100, + y: 200, + }, + 42000, { x: 200, y: 100, oldX: 100, oldY: 200, moving: false, - direction: "up" + direction: "up", }, 42200 ); @@ -69,22 +74,25 @@ describe("Interpolation / Extrapolation", () => { y: 100, oldX: 100, oldY: 200, - direction: 'up', - moving: false + direction: "up", + moving: false, }); }); it("should keep moving until it stops", () => { - const playerMovement = new PlayerMovement({ - x: 100, y: 200 - }, 42000, + const playerMovement = new PlayerMovement( + { + x: 100, + y: 200, + }, + 42000, { x: 200, y: 100, oldX: 100, oldY: 200, moving: false, - direction: "up" + direction: "up", }, 42200 ); @@ -94,8 +102,8 @@ describe("Interpolation / Extrapolation", () => { y: 150, oldX: 100, oldY: 200, - direction: 'up', - moving: false + direction: "up", + moving: false, }); }); -}) +}); diff --git a/front/yarn.lock b/front/yarn.lock index 1557a08e..faf5e681 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -150,6 +150,59 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + "@sentry/types@^6.11.0": version "6.12.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.12.0.tgz#b7395688a79403c6df8d8bb8d81deb8222519853" @@ -293,6 +346,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/long@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -317,11 +375,26 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.3.0.tgz#d6fed7d6bc6854306da3dea1af9f874b00783e26" integrity sha512-8/bnjSZD86ZfpBsDlCIkNXIvm+h6wi9g7IqL+kmFkQ+Wvu3JrasgLElfiPgoo8V8vVfnEi0QVS12gbl94h9YsQ== +"@types/node@>=13.7.0": + version "17.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.5.tgz#57ca67ec4e57ad9e4ef5a6bab48a15387a1c83e0" + integrity sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw== + +"@types/object-hash@^1.3.0": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.4.tgz#079ba142be65833293673254831b5e3e847fe58b" + integrity sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA== + "@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/prettier@^1.19.0": + version "1.19.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" + integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ== + "@types/pug@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.4.tgz#8772fcd0418e3cd2cc171555d73007415051f4b2" @@ -1656,6 +1729,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +dataloader@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" + integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3662,7 +3740,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20: +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3695,6 +3773,11 @@ lokijs@^1.5.12: resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.12.tgz#cb55b37009bdf09ee7952a6adddd555b893653a0" integrity sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q== +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + lower-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" @@ -4098,6 +4181,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" + integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== + object-inspect@^1.9.0: version "1.10.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" @@ -4590,6 +4678,11 @@ prettier-plugin-svelte@^2.5.0: resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.5.0.tgz#7922534729f7febe59b4c56c3f5360539f0d8ab1" integrity sha512-+iHY2uGChOngrgKielJUnqo74gIL/EO5oeWm8MftFWjEi213lq9QYTOwm1pv4lI1nA61tdgf80CF2i5zMcu1kw== +prettier@^2.0.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" + integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== + prettier@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" @@ -4618,6 +4711,25 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +protobufjs@^6.8.8: + version "6.11.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" + integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -5819,6 +5931,35 @@ ts-node@^10.4.0: make-error "^1.1.1" yn "3.1.1" +ts-poet@^4.5.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.6.1.tgz#015dc823d726655af9f095c900f84ed7c60e2dd3" + integrity sha512-DXJ+mBJIDp+jiaUgB4N5I/sczHHDU2FWacdbDNVAVS4Mh4hb7ckpvUWVW7m7/nAOcjR0r4Wt+7AoO7FeJKExfA== + dependencies: + "@types/prettier" "^1.19.0" + lodash "^4.17.15" + prettier "^2.0.2" + +ts-proto-descriptors@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.3.1.tgz#760ebaaa19475b03662f7b358ffea45b9c5348f5" + integrity sha512-Cybb3fqceMwA6JzHdC32dIo8eVGVmXrM6TWhdk1XQVVHT/6OQqk0ioyX1dIdu3rCIBhRmWUhUE4HsyK+olmgMw== + dependencies: + long "^4.0.0" + protobufjs "^6.8.8" + +ts-proto@^1.96.0: + version "1.96.0" + resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.96.0.tgz#63768d7da533b337aee84db065dd66773bd4cac9" + integrity sha512-fKwaGzi8EOCU9xwmcXK917jj1WhFdLbFkPRawQ+5CAZM9eSXr/mpkz/yEctXCiuei364z6jAB2Odb64KCDFTPQ== + dependencies: + "@types/object-hash" "^1.3.0" + dataloader "^1.4.0" + object-hash "^1.3.1" + protobufjs "^6.8.8" + ts-poet "^4.5.0" + ts-proto-descriptors "^1.2.1" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" diff --git a/maps/tests/EmbeddedWebsite/website_in_map_script.php b/maps/tests/EmbeddedWebsite/website_in_map_script.php index c822b8ca..78eced1a 100644 --- a/maps/tests/EmbeddedWebsite/website_in_map_script.php +++ b/maps/tests/EmbeddedWebsite/website_in_map_script.php @@ -15,6 +15,8 @@ const heightField = document.getElementById('height'); const urlField = document.getElementById('url'); const visibleField = document.getElementById('visible'); + const originField = document.getElementById('origin'); + const scaleField = document.getElementById('scale'); createButton.addEventListener('click', () => { console.log('CREATING NEW EMBEDDED IFRAME'); @@ -28,6 +30,8 @@ height: parseInt(heightField.value), }, visible: !!visibleField.value, + origin: originField.value, + scale: parseFloat(scaleField.value), }); }); @@ -61,6 +65,16 @@ const website = await WA.room.website.get('test'); website.visible = this.checked; }); + + originField.addEventListener('change', async function() { + const website = await WA.room.website.get('test'); + website.origin = this.value; + }); + + scaleField.addEventListener('change', async function() { + const website = await WA.room.website.get('test'); + website.scale = parseFloat(this.value); + }); }); }) </script> @@ -72,6 +86,8 @@ width: <input type="text" id="width" value="600" /><br/> height: <input type="text" id="height" value="400" /><br/> URL: <input type="text" id="url" value="https://mensuel.framapad.org/p/rt6c904745-9oxm?lang=en" /><br/> Visible: <input type="checkbox" id="visible" value=1 /><br/> +Origin: <input type="text" id="origin" value="map" /><br/> +Scale: <input type="text" id="scale" value=1 /><br/> <button id="createEmbeddedWebsite">Create embedded website</button> diff --git a/messages/JsonMessages/MapDetailsData.ts b/messages/JsonMessages/MapDetailsData.ts index 794c9bac..39866eee 100644 --- a/messages/JsonMessages/MapDetailsData.ts +++ b/messages/JsonMessages/MapDetailsData.ts @@ -20,6 +20,10 @@ export const isMapDetailsData = new tg.IsInterface() }) .withOptionalProperties({ iframeAuthentication: tg.isNullable(tg.isString), + // The date (in ISO 8601 format) at which the room will expire + expireOn: tg.isString, + // Whether the "report" feature is enabled or not on this room + canReport: tg.isBoolean, }) .get(); diff --git a/messages/package.json b/messages/package.json index 6095a626..e6f04ba5 100644 --- a/messages/package.json +++ b/messages/package.json @@ -4,13 +4,14 @@ "description": "", "scripts": { "proto": "grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --grpc_out=generated --js_out=\"import_style=commonjs,binary:generated\" --ts_out=generated -I ./protos protos/*.proto", + "ts-proto": "grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=ts-proto-generated --ts_proto_opt=oneof=unions --ts_proto_opt=esModuleInterop=true protos/*.proto", "copy-to-back": "rm -rf ../back/src/Messages/generated && cp -rf generated/ ../back/src/Messages/generated", - "copy-to-front": "rm -rf ../front/src/Messages/generated && cp -rf generated/ ../front/src/Messages/generated", + "copy-to-front-ts-proto": "sed 's/import { Observable } from \"rxjs\";/import type { Observable } from \"rxjs\";/g' ts-proto-generated/protos/messages.ts > ../front/src/Messages/ts-proto-generated/messages.ts", "copy-to-pusher": "rm -rf ../pusher/src/Messages/generated && cp -rf generated/ ../pusher/src/Messages/generated", "json-copy-to-pusher": "rm -rf ../pusher/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../pusher/src/Messages/JsonMessages/", "json-copy-to-front": "rm -rf ../front/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../front/src/Messages/JsonMessages/", "precommit": "lint-staged", - "proto-all": "yarn run proto && yarn run copy-to-back && yarn run copy-to-front && yarn run copy-to-pusher && yarn run json-copy-to-pusher && yarn run json-copy-to-front", + "proto-all": "yarn run proto && yarn run ts-proto && yarn run copy-to-back && yarn run copy-to-front-ts-proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher && yarn run json-copy-to-front", "proto:watch": "yarn run proto-all; inotifywait -q -m -e close_write protos/messages.proto JsonMessages/ | while read -r filename event; do yarn run proto-all; done", "pretty": "yarn prettier --write 'JsonMessages/**/*.ts'", "pretty-check": "yarn prettier --check 'JsonMessages/**/*.ts'" @@ -18,7 +19,8 @@ "dependencies": { "generic-type-guard": "^3.5.0", "google-protobuf": "^3.13.0", - "grpc": "^1.24.4" + "grpc": "^1.24.4", + "ts-proto": "^1.96.0" }, "devDependencies": { "@types/google-protobuf": "^3.7.4", diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 3c05037a..8ac7bbf0 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -296,8 +296,6 @@ message ServerToClientMessage { WebRtcSignalToClientMessage webRtcSignalToClientMessage = 5; WebRtcSignalToClientMessage webRtcScreenSharingSignalToClientMessage = 6; WebRtcDisconnectMessage webRtcDisconnectMessage = 7; - PlayGlobalMessage playGlobalMessage = 8; - StopGlobalMessage stopGlobalMessage = 9; TeleportMessageMessage teleportMessageMessage = 10; SendJitsiJwtMessage sendJitsiJwtMessage = 11; SendUserMessage sendUserMessage = 12; @@ -390,8 +388,6 @@ message PusherToBackMessage { SetPlayerDetailsMessage setPlayerDetailsMessage = 5; WebRtcSignalToServerMessage webRtcSignalToServerMessage = 6; WebRtcSignalToServerMessage webRtcScreenSharingSignalToServerMessage = 7; - PlayGlobalMessage playGlobalMessage = 8; - StopGlobalMessage stopGlobalMessage = 9; ReportPlayerMessage reportPlayerMessage = 10; QueryJitsiJwtMessage queryJitsiJwtMessage = 11; SendUserMessage sendUserMessage = 12; diff --git a/messages/ts-proto-generated/.gitignore b/messages/ts-proto-generated/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/messages/ts-proto-generated/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/messages/yarn.lock b/messages/yarn.lock index 5d040c6f..8ba2fe90 100644 --- a/messages/yarn.lock +++ b/messages/yarn.lock @@ -174,6 +174,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== +"@types/node@>=13.7.0": + version "17.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.5.tgz#57ca67ec4e57ad9e4ef5a6bab48a15387a1c83e0" + integrity sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw== + "@types/node@^12.12.29": version "12.19.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" @@ -184,6 +189,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.30.tgz#1ed6e01e4ca576d5aec9cc802cc3bcf94c274192" integrity sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA== +"@types/object-hash@^1.3.0": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.4.tgz#079ba142be65833293673254831b5e3e847fe58b" + integrity sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA== + "@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" @@ -194,6 +204,11 @@ resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.4.tgz#7639e16015440d9baf622f83c12dae47787226b7" integrity sha512-M56NfQHfaWuaj6daSgCVs7jh8fXLI3LmxjRoQxmOvYesgIkI+9HPsDLO0vd7wX7cwA0D0ZWFEJdp0VPwLdS+bQ== +"@types/prettier@^1.19.0": + version "1.19.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" + integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ== + "@typescript-eslint/eslint-plugin@^4.7.0": version "4.7.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.7.0.tgz#85c9bbda00c0cb604d3c241f7bc7fb171a2d3479" @@ -1156,6 +1171,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +dataloader@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" + integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== + date.js@^0.3.1: version "0.3.3" resolved "https://registry.yarnpkg.com/date.js/-/date.js-0.3.3.tgz#ef1e92332f507a638795dbb985e951882e50bbda" @@ -3154,6 +3174,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" + integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== + object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" @@ -3423,7 +3448,7 @@ 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: +prettier@^2.0.2, prettier@^2.3.1: version "2.5.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== @@ -3467,6 +3492,25 @@ protobufjs@^6.10.1: "@types/node" "^13.7.0" long "^4.0.0" +protobufjs@^6.8.8: + version "6.11.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" + integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -4245,6 +4289,35 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +ts-poet@^4.5.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.6.1.tgz#015dc823d726655af9f095c900f84ed7c60e2dd3" + integrity sha512-DXJ+mBJIDp+jiaUgB4N5I/sczHHDU2FWacdbDNVAVS4Mh4hb7ckpvUWVW7m7/nAOcjR0r4Wt+7AoO7FeJKExfA== + dependencies: + "@types/prettier" "^1.19.0" + lodash "^4.17.15" + prettier "^2.0.2" + +ts-proto-descriptors@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.3.1.tgz#760ebaaa19475b03662f7b358ffea45b9c5348f5" + integrity sha512-Cybb3fqceMwA6JzHdC32dIo8eVGVmXrM6TWhdk1XQVVHT/6OQqk0ioyX1dIdu3rCIBhRmWUhUE4HsyK+olmgMw== + dependencies: + long "^4.0.0" + protobufjs "^6.8.8" + +ts-proto@^1.96.0: + version "1.96.0" + resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.96.0.tgz#63768d7da533b337aee84db065dd66773bd4cac9" + integrity sha512-fKwaGzi8EOCU9xwmcXK917jj1WhFdLbFkPRawQ+5CAZM9eSXr/mpkz/yEctXCiuei364z6jAB2Odb64KCDFTPQ== + dependencies: + "@types/object-hash" "^1.3.0" + dataloader "^1.4.0" + object-hash "^1.3.1" + protobufjs "^6.8.8" + ts-poet "^4.5.0" + ts-proto-descriptors "^1.2.1" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"