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/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> { 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 +``` +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; 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 :
@@ -12,6 +15,7 @@ Example :
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 ``` @@ -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 `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 ``` + 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 ``` -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 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/Dockerfile b/front/Dockerfile index 507f0a0b..831de1ef 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -2,7 +2,7 @@ FROM node:14-buster-slim as messages WORKDIR /usr/src COPY messages . -RUN yarn install && yarn proto +RUN yarn install && yarn ts-proto # webpack build FROM node:14-buster-slim as builder @@ -10,9 +10,11 @@ WORKDIR /usr/src COPY front/yarn.lock front/package.json ./ RUN yarn install --ignore-engines -COPY front . -COPY --from=messages /usr/src/generated src/Messages/generated -COPY --from=messages /usr/src/JsonMessages src/Messages/JsonMessages + +COPY --chown=docker:docker front . +COPY --from=messages --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=messages --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages # passing arguments as environment ARG DEBUG_MODE diff --git a/front/package.json b/front/package.json index 9e61fc7d..1cf577bf 100644 --- a/front/package.json +++ b/front/package.json @@ -64,6 +64,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/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 b92c6b37..24ae6fae 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"; import { isGetPropertyEvent } from "./GetPropertyEvent"; export interface TypedMessageEvent extends MessageEvent { @@ -51,6 +53,7 @@ export type IframeEventMap = { displayBubble: null; removeBubble: null; onPlayerMove: undefined; + onCameraUpdate: undefined; showLayer: LayerEvent; hideLayer: LayerEvent; setProperty: SetPropertyEvent; @@ -83,6 +86,7 @@ export interface IframeResponseEventMap { leaveZoneEvent: ChangeZoneEvent; buttonClickedEvent: ButtonClickedEvent; hasPlayerMoved: HasPlayerMovedEvent; + wasCameraUpdated: WasCameraUpdatedEvent; menuItemClicked: MenuItemClickedEvent; setVariable: SetVariableEvent; messageTriggered: MessageReferenceEvent; @@ -166,6 +170,10 @@ export const iframeQueryMapTypeGuards = { query: tg.isUndefined, answer: tg.isUndefined, }, + getPlayerPosition: { + query: tg.isUndefined, + answer: isPlayerPosition, + }, }; type GuardedType = 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; 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; + +export type WasCameraUpdatedEventCallback = (event: WasCameraUpdatedEvent) => void; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 16b6138c..2c725081 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 = ( @@ -88,6 +89,9 @@ class IframeListener { private readonly _loadSoundStream: Subject = new Subject(); public readonly loadSoundStream = this._loadSoundStream.asObservable(); + private readonly _trackCameraUpdateStream: Subject = new Subject(); + public readonly trackCameraUpdateStream = this._trackCameraUpdateStream.asObservable(); + private readonly _setTilesStream: Subject = new Subject(); public readonly setTilesStream = this._setTilesStream.asObservable(); @@ -229,6 +233,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)) { @@ -448,6 +454,13 @@ class IframeListener { } } + sendCameraUpdated(event: WasCameraUpdatedEvent) { + this.postMessage({ + type: "wasCameraUpdated", + data: event, + }); + } + sendButtonClickedEvent(popupId: number, buttonId: number, input : boolean, inputValue : string | null): 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/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(); + +export class WorkAdventureCameraCommands extends IframeApiContribution { + callbacks = [ + apiCallback({ + type: "wasCameraUpdated", + typeChecker: isWasCameraUpdatedEvent, + callback: (payloadData) => { + moveStream.next(payloadData); + }, + }), + ]; + + onCameraUpdate(): Subject { + 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(); @@ -31,6 +32,8 @@ export const setUuid = (_uuid: string | undefined) => { }; export class WorkadventurePlayerCommands extends IframeApiContribution { + readonly state = createState("player"); + callbacks = [ apiCallback({ type: "hasPlayerMoved", @@ -74,6 +77,13 @@ export class WorkadventurePlayerCommands extends IframeApiContribution { + 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(); -const variables = new Map(); -const variableSubscribers = new Map>(); - -export const initVariables = (_variables: Map): 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 { + private setVariableResolvers = new Subject(); + private variables = new Map(); + private variableSubscribers = new Map>(); + + 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): 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 { - 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 { - let subject = variableSubscribers.get(key); + let subject = this.variableSubscribers.get(key); if (subject === undefined) { subject = new Subject(); - 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); 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/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 -{#if $followStateStore === "requesting"} +{#if $followStateStore === "requesting" && $followRoleStore === "follower"}
- {#if $followRoleStore === "follower"} -
-

Do you want to follow {name($followUsersStore[0])}?

-
-
- - -
- {:else if $followRoleStore === "leader"} -
-

Should never be displayed

-
- {/if} +
+

Do you want to follow {name($followUsersStore[0])}?

+
+
+ + +
{/if} 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 020173ec..5d361d59 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -281,7 +281,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( @@ -293,7 +293,7 @@ class ConnectionManager { ); }); - connection.onConnect((connect: OnConnectInterface) => { + connection.roomJoinedMessageStream.subscribe((connect: OnConnectInterface) => { resolve(connect); }); }).catch((err) => { 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 = 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 f86a4258..314873b3 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) { @@ -223,6 +223,27 @@ class LocalUserStore { const cameraSetupValues = localStorage.getItem(cameraSetup); return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; } + + getAllUserProperties(): Map { + const result = new Map(); + 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/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 328f1aec..96c6dd23 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(); + 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(); + public readonly webRtcStartMessageStream = this._webRtcStartMessageStream.asObservable(); + + private readonly _webRtcSignalToClientMessageStream = new Subject(); + public readonly webRtcSignalToClientMessageStream = this._webRtcSignalToClientMessageStream.asObservable(); + + private readonly _webRtcScreenSharingSignalToClientMessageStream = + new Subject(); + public readonly webRtcScreenSharingSignalToClientMessageStream = + this._webRtcScreenSharingSignalToClientMessageStream.asObservable(); + + private readonly _webRtcDisconnectMessageStream = new Subject(); + public readonly webRtcDisconnectMessageStream = this._webRtcDisconnectMessageStream.asObservable(); + + private readonly _teleportMessageMessageStream = new Subject(); + public readonly teleportMessageMessageStream = this._teleportMessageMessageStream.asObservable(); + + private readonly _sendJitsiJwtMessageStream = new Subject(); + public readonly sendJitsiJwtMessageStream = this._sendJitsiJwtMessageStream.asObservable(); + + private readonly _worldFullMessageStream = new Subject(); + public readonly worldFullMessageStream = this._worldFullMessageStream.asObservable(); + + private readonly _worldConnexionMessageStream = new Subject(); + public readonly worldConnexionMessageStream = this._worldConnexionMessageStream.asObservable(); + + private readonly _tokenExpiredMessageStream = new Subject(); + public readonly tokenExpiredMessageStream = this._tokenExpiredMessageStream.asObservable(); + + private readonly _userMovedMessageStream = new Subject(); + public readonly userMovedMessageStream = this._userMovedMessageStream.asObservable(); + + private readonly _groupUpdateMessageStream = new Subject(); + public readonly groupUpdateMessageStream = this._groupUpdateMessageStream.asObservable(); + + private readonly _groupDeleteMessageStream = new Subject(); + public readonly groupDeleteMessageStream = this._groupDeleteMessageStream.asObservable(); + + private readonly _userJoinedMessageStream = new Subject(); + public readonly userJoinedMessageStream = this._userJoinedMessageStream.asObservable(); + + private readonly _userLeftMessageStream = new Subject(); + 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(); + 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(); + public readonly playerDetailsUpdatedMessageStream = this._playerDetailsUpdatedMessageStream.asObservable(); + + private readonly _connectionErrorStream = new Subject(); + 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(); + 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(); + 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(); - 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 = new Subject(); - 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/generated/.gitignore b/front/src/Messages/generated/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/front/src/Messages/generated/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore 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/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/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 34a6dbb7..93a68dbc 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(); private embeddedWebsiteManager!: EmbeddedWebsiteManager; private loader: Loader; + private lastCameraEvent: WasCameraUpdatedEvent | undefined; + private firstCameraUpdateSent: boolean = false; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -454,10 +455,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"; @@ -540,7 +537,9 @@ export class GameScene extends DirtyScene { object.height, object.visible, allowApi ?? false, - "" + "", + "map", + 1 ); } } @@ -634,8 +633,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; @@ -710,7 +707,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, @@ -724,31 +721,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); } @@ -760,7 +759,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( @@ -773,18 +772,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 @@ -859,6 +869,8 @@ export class GameScene extends DirtyScene { }); }); + this.emoteManager = new EmoteManager(this, this.connection); + // this.gameMap.onLeaveLayer((layers) => { // layers.forEach((layer) => { // iframeListener.sendLeaveLayerEvent(layer.name); @@ -1121,9 +1133,33 @@ export class GameScene extends DirtyScene { ); 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); + } }) ); @@ -1186,6 +1222,12 @@ export class GameScene extends DirtyScene { }) ); + 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"); @@ -1261,6 +1303,7 @@ export class GameScene extends DirtyScene { roomId: this.roomUrl, tags: this.connection ? this.connection.getAllTags() : [], variables: this.sharedVariablesManager.variables, + playerVariables: localUserStore.getAllUserProperties(), userRoomToken: this.connection ? this.connection.userRoomToken : "", }; }); @@ -1358,6 +1401,22 @@ export class GameScene extends DirtyScene { }) ); + 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); }); @@ -1376,6 +1435,13 @@ export class GameScene extends DirtyScene { this.CurrentPlayer.removeOutlineColor(); this.connection?.emitPlayerOutlineColor(null); }); + + iframeListener.registerAnswerer("getPlayerPosition", () => { + return { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y, + }; + }); } private setPropertyLayer( @@ -1500,6 +1566,7 @@ export class GameScene extends DirtyScene { iframeListener.unregisterAnswerer("openCoWebsite"); iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("setPlayerOutline"); + iframeListener.unregisterAnswerer("setVariable"); this.sharedVariablesManager?.close(); this.embeddedWebsiteManager?.close(); @@ -1978,6 +2045,7 @@ export class GameScene extends DirtyScene { this.loader.resize(); } + private getObjectLayerData(objectName: string): ITiledMapObject | undefined { for (const layer of this.mapFile.layers) { if (layer.type === "objectgroup" && layer.name === "floorLayer") { @@ -1990,6 +2058,7 @@ export class GameScene extends DirtyScene { } return undefined; } + private reposition(): void { // Recompute camera offset if needed biggestAvailableAreaStore.recompute(); 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 { 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/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(); 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/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 769eba93..82de9837 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -14,25 +14,29 @@ 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); - 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); + player.state.initVariables(gameState.playerVariables as Map); + setUserRoomToken(gameState.userRoomToken); }); const wa = { @@ -43,7 +47,8 @@ const wa = { sound, room, player, - state, + camera, + state: globalState, onInit(): Promise { return initPromise; @@ -225,7 +230,5 @@ window.addEventListener( callback?.callback(payloadData); } } - - // ... } ); diff --git a/front/style/style.scss b/front/style/style.scss index ccfdd6c0..4a95399d 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -1092,3 +1092,7 @@ div.is-silent.hide { height: 1px !important; width: 1px !important; } + +.emote-menu .emoji-picker .emoji-picker__emoji { + font-family: 'Twemoji Mozilla' !important; +} \ No newline at end of file diff --git a/front/yarn.lock b/front/yarn.lock index ae5f4017..d0ae6188 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" @@ -3702,7 +3780,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== @@ -3735,6 +3813,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" @@ -4143,6 +4226,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" @@ -4649,6 +4737,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" @@ -4677,6 +4770,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" @@ -5896,6 +6008,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); + }); }); }) @@ -72,6 +86,8 @@ width:
height:
URL:
Visible:
+Origin:
+Scale:
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"