Merge branch 'develop' of github.com:thecodingmachine/workadventure into develop

This commit is contained in:
Lurkars 2021-07-21 19:28:00 +02:00
commit fb2bd1c346
130 changed files with 3561 additions and 2772 deletions

3
.gitignore vendored
View File

@ -7,4 +7,5 @@ docker-compose.override.yaml
maps/yarn.lock maps/yarn.lock
maps/dist/computer.js maps/dist/computer.js
maps/dist/computer.js.map maps/dist/computer.js.map
/node_modules/ node_modules
_

View File

@ -10,12 +10,23 @@
- New scripting API features : - New scripting API features :
- Use `WA.room.showLayer(): void` to show a layer - Use `WA.room.showLayer(): void` to show a layer
- Use `WA.room.hideLayer(): void` to hide a layer - Use `WA.room.hideLayer(): void` to hide a layer
- Use `WA.room.setProperty() : void` to add or change existing property of a layer - Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player - Use `WA.player.onPlayerMove(): void` to track the movement of the current player
- Use `WA.room.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player - Use `WA.player.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu - Use `WA.ui.registerMenuCommand(): void` to add a custom menu
- Use `WA.room.setTiles(): void` to change an array of tiles - Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
- The text chat was redesigned to be prettier and to use more features :
- The chat is now persistent bewteen discussions and always accesible
- The chat now tracks incoming and outcoming users in your conversation
- The chat allows your to see the visit card of users
- You can close the chat window with the escape key
- Added a 'Enable notifications' button in the menu.
- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed:
- `/api/map`: now accepts a complete room URL instead of organization/world/room slugs
- `/api/ban`: new endpoint to report users
- as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing
## Version 1.4.3 - 1.4.4 - 1.4.5 ## Version 1.4.3 - 1.4.4 - 1.4.5

View File

@ -42,7 +42,7 @@ Before committing, be sure to install the "Prettier" precommit hook that will re
In order to enable the "Prettier" precommit hook, at the root of the project, run: In order to enable the "Prettier" precommit hook, at the root of the project, run:
```console ```console
$ yarn run install $ yarn install
$ yarn run prepare $ yarn run prepare
``` ```

View File

@ -15,7 +15,7 @@ export class DebugController {
const query = parse(req.getQuery()); const query = parse(req.getQuery());
if (query.token !== ADMIN_API_TOKEN) { if (query.token !== ADMIN_API_TOKEN) {
return res.status(401).send("Invalid token sent!"); return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
} }
return res return res

View File

@ -5,8 +5,6 @@ import { PositionInterface } from "_Model/PositionInterface";
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone"; import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
import { PositionNotifier } from "./PositionNotifier"; import { PositionNotifier } from "./PositionNotifier";
import { Movable } from "_Model/Movable"; import { Movable } from "_Model/Movable";
import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
import { arrayIntersect } from "../Services/ArrayHelper";
import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb"; import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { ZoneSocket } from "src/RoomManager"; import { ZoneSocket } from "src/RoomManager";
@ -15,12 +13,6 @@ import { Admin } from "../Model/Admin";
export type ConnectCallback = (user: User, group: Group) => void; export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void;
export enum GameRoomPolicyTypes {
ANONYMOUS_POLICY = 1,
MEMBERS_ONLY_POLICY,
USE_TAGS_POLICY,
}
export class GameRoom { export class GameRoom {
private readonly minDistance: number; private readonly minDistance: number;
private readonly groupRadius: number; private readonly groupRadius: number;
@ -37,15 +29,12 @@ export class GameRoom {
private itemsState: Map<number, unknown> = new Map<number, unknown>(); private itemsState: Map<number, unknown> = new Map<number, unknown>();
private readonly positionNotifier: PositionNotifier; private readonly positionNotifier: PositionNotifier;
public readonly roomId: string; public readonly roomUrl: string;
public readonly roomSlug: string;
public readonly worldSlug: string = "";
public readonly organizationSlug: string = "";
private versionNumber: number = 1; private versionNumber: number = 1;
private nextUserId: number = 1; private nextUserId: number = 1;
constructor( constructor(
roomId: string, roomUrl: string,
connectCallback: ConnectCallback, connectCallback: ConnectCallback,
disconnectCallback: DisconnectCallback, disconnectCallback: DisconnectCallback,
minDistance: number, minDistance: number,
@ -55,16 +44,7 @@ export class GameRoom {
onLeaves: LeavesCallback, onLeaves: LeavesCallback,
onEmote: EmoteCallback onEmote: EmoteCallback
) { ) {
this.roomId = roomId; this.roomUrl = roomUrl;
if (isRoomAnonymous(roomId)) {
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
} else {
const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
this.roomSlug = roomSlug;
this.organizationSlug = organizationSlug;
this.worldSlug = worldSlug;
}
this.users = new Map<number, User>(); this.users = new Map<number, User>();
this.usersByUuid = new Map<string, User>(); this.usersByUuid = new Map<string, User>();
@ -183,7 +163,7 @@ export class GameRoom {
} else { } else {
const closestUser: User = closestItem; const closestUser: User = closestItem;
const group: Group = new Group( const group: Group = new Group(
this.roomId, this.roomUrl,
[user, closestUser], [user, closestUser],
this.connectCallback, this.connectCallback,
this.disconnectCallback, this.disconnectCallback,

View File

@ -1,30 +0,0 @@
//helper functions to parse room IDs
export const isRoomAnonymous = (roomID: string): boolean => {
if (roomID.startsWith("_/")) {
return true;
} else if (roomID.startsWith("@/")) {
return false;
} else {
throw new Error("Incorrect room ID: " + roomID);
}
};
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
const idParts = roomId.split("/");
if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId);
return idParts.slice(2).join("/");
};
export interface extractDataFromPrivateRoomIdResponse {
organizationSlug: string;
worldSlug: string;
roomSlug: string;
}
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
const idParts = roomId.split("/");
if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId);
const organizationSlug = idParts[1];
const worldSlug = idParts[2];
const roomSlug = idParts[3];
return { organizationSlug, worldSlug, roomSlug };
};

View File

@ -250,12 +250,12 @@ export class SocketManager {
//user leave previous world //user leave previous world
room.leave(user); room.leave(user);
if (room.isEmpty()) { if (room.isEmpty()) {
this.rooms.delete(room.roomId); this.rooms.delete(room.roomUrl);
gaugeManager.decNbRoomGauge(); gaugeManager.decNbRoomGauge();
debug('Room is empty. Deleting room "%s"', room.roomId); debug('Room is empty. Deleting room "%s"', room.roomUrl);
} }
} finally { } finally {
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
console.log("A user left"); console.log("A user left");
} }
} }
@ -308,6 +308,7 @@ export class SocketManager {
throw new Error("clientUser.userId is not an integer " + thing.id); throw new Error("clientUser.userId is not an integer " + thing.id);
} }
userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setUseruuid(thing.uuid);
userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setName(thing.name);
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
@ -425,7 +426,6 @@ export class SocketManager {
// Let's send 2 messages: one to the user joining the group and one to the other user // Let's send 2 messages: one to the user joining the group and one to the other user
const webrtcStartMessage1 = new WebRtcStartMessage(); const webrtcStartMessage1 = new WebRtcStartMessage();
webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setName(otherUser.name);
webrtcStartMessage1.setInitiator(true); webrtcStartMessage1.setInitiator(true);
if (TURN_STATIC_AUTH_SECRET !== "") { if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
@ -436,14 +436,10 @@ export class SocketManager {
const serverToClientMessage1 = new ServerToClientMessage(); const serverToClientMessage1 = new ServerToClientMessage();
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
//if (!user.socket.disconnecting) {
user.socket.write(serverToClientMessage1); user.socket.write(serverToClientMessage1);
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
//}
const webrtcStartMessage2 = new WebRtcStartMessage(); const webrtcStartMessage2 = new WebRtcStartMessage();
webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setName(user.name);
webrtcStartMessage2.setInitiator(false); webrtcStartMessage2.setInitiator(false);
if (TURN_STATIC_AUTH_SECRET !== "") { if (TURN_STATIC_AUTH_SECRET !== "") {
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
@ -454,10 +450,7 @@ export class SocketManager {
const serverToClientMessage2 = new ServerToClientMessage(); const serverToClientMessage2 = new ServerToClientMessage();
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
//if (!otherUser.socket.disconnecting) {
otherUser.socket.write(serverToClientMessage2); otherUser.socket.write(serverToClientMessage2);
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
//}
} }
} }
@ -614,6 +607,7 @@ export class SocketManager {
if (thing instanceof User) { if (thing instanceof User) {
const userJoinedMessage = new UserJoinedZoneMessage(); const userJoinedMessage = new UserJoinedZoneMessage();
userJoinedMessage.setUserid(thing.id); userJoinedMessage.setUserid(thing.id);
userJoinedMessage.setUseruuid(thing.uuid);
userJoinedMessage.setName(thing.name); userJoinedMessage.setName(thing.name);
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
@ -664,9 +658,9 @@ export class SocketManager {
public leaveAdminRoom(room: GameRoom, admin: Admin) { public leaveAdminRoom(room: GameRoom, admin: Admin) {
room.adminLeave(admin); room.adminLeave(admin);
if (room.isEmpty()) { if (room.isEmpty()) {
this.rooms.delete(room.roomId); this.rooms.delete(room.roomUrl);
gaugeManager.decNbRoomGauge(); gaugeManager.decNbRoomGauge();
debug('Room is empty. Deleting room "%s"', room.roomId); debug('Room is empty. Deleting room "%s"', room.roomUrl);
} }
} }

View File

@ -1,19 +0,0 @@
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
describe("RoomIdentifier", () => {
it("should flag public id as anonymous", () => {
expect(isRoomAnonymous('_/global/test')).toBe(true);
});
it("should flag public id as not anonymous", () => {
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
});
it("should extract roomSlug from public ID", () => {
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
});
it("should extract correct from private ID", () => {
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
expect(organizationSlug).toBe('afup');
expect(worldSlug).toBe('afup2020');
expect(roomSlug).toBe('1floor');
});
})

View File

@ -1,6 +1,7 @@
{.section-title.accent.text-primary} {.section-title.accent.text-primary}
# API Player functions Reference # API Player functions Reference
### Listen to player movement ### Listen to player movement
``` ```
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;

View File

@ -54,6 +54,7 @@ WA.room.showLayer(layerName : string): void
WA.room.hideLayer(layerName : string) : void WA.room.hideLayer(layerName : string) : void
``` ```
These 2 methods can be used to show and hide a 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 : Example :
```javascript ```javascript
@ -70,49 +71,14 @@ WA.room.setProperty(layerName : string, propertyName : string, propertyValue : s
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 : Example :
```javascript ```javascript
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
``` ```
### Getting information on the current room
```
WA.room.getCurrentRoom(): Promise<Room>
```
Return a promise that resolves to a `Room` object with the following attributes :
* **id (string) :** ID of the current room
* **map (ITiledMap) :** contains the JSON map file with the properties that were setted by the script if `setProperty` was called.
* **mapUrl (string) :** Url of the JSON map file
* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer
Example :
```javascript
WA.room.getCurrentRoom((room) => {
if (room.id === '42') {
console.log(room.map);
window.open(room.mapUrl, '_blank');
}
})
```
### Getting information on the current user
```
WA.player.getCurrentUser(): Promise<User>
```
Return a promise that resolves to a `User` object with the following attributes :
* **id (string) :** ID of the current user
* **nickName (string) :** name displayed above the current user
* **tags (string[]) :** list of all the tags of the current user
Example :
```javascript
WA.room.getCurrentUser().then((user) => {
if (user.nickName === 'ABC') {
console.log(user.tags);
}
})
```
### Changing tiles ### Changing tiles
``` ```
WA.room.setTiles(tiles: TileDescriptor[]): void WA.room.setTiles(tiles: TileDescriptor[]): void
@ -134,6 +100,7 @@ If `tile` is a string, it's not the id of the tile but the value of the property
**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 : Example :
```javascript ```javascript

View File

@ -37,8 +37,7 @@
<div class="main-container" id="main-container"> <div class="main-container" id="main-container">
<!-- Create the editor container --> <!-- Create the editor container -->
<div id="game" class="game"> <div id="game" class="game">
<div id="svelte-overlay"> <div id="svelte-overlay"></div>
</div>
<div id="game-overlay" class="game-overlay"> <div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section"> <div id="main-section" class="main-section">
</div> </div>

View File

@ -57,6 +57,9 @@
<section> <section>
<button id="toggleFullscreen">Toggle fullscreen</button> <button id="toggleFullscreen">Toggle fullscreen</button>
</section> </section>
<section>
<button id="enableNotification">Enable notifications</button>
</section>
<section> <section>
<button id="sparkButton">Create map</button> <button id="sparkButton">Create map</button>
</section> </section>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

53
front/dist/resources/service-worker.js vendored Normal file
View File

@ -0,0 +1,53 @@
let CACHE_NAME = 'workavdenture-cache-v1';
let urlsToCache = [
'/'
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request).then(
function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two streams.
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
self.addEventListener('activate', function(event) {
//TODO activate service worker
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -119,7 +119,13 @@
"src": "/static/images/favicons/android-icon-192x192.png", "src": "/static/images/favicons/android-icon-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image\/png", "type": "image\/png",
"density": "4.0" "density": "4.0",
"purpose": "any maskable"
},
{
"src": "/static/images/favicons/icon-512x512.png",
"sizes": "512x512",
"type": "image\/png"
} }
], ],
"start_url": "/", "start_url": "/",
@ -127,6 +133,7 @@
"display_override": ["window-control-overlay", "minimal-ui"], "display_override": ["window-control-overlay", "minimal-ui"],
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"lang": "en",
"theme_color": "#000000", "theme_color": "#000000",
"shortcuts": [ "shortcuts": [
{ {
@ -134,7 +141,7 @@
"short_name": "WA", "short_name": "WA",
"description": "WorkAdventure application", "description": "WorkAdventure application",
"url": "/", "url": "/",
"icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192" }] "icons": [{ "src": "/static/images/favicons/android-icon-192x192.png", "sizes": "192x192", "type": "image/png" }]
} }
], ],
"description": "WorkAdventure application", "description": "WorkAdventure application",

BIN
front/dist/static/images/send.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -39,7 +39,7 @@
}, },
"dependencies": { "dependencies": {
"@fontsource/press-start-2p": "^4.3.0", "@fontsource/press-start-2p": "^4.3.0",
"@types/simple-peer": "^9.6.0", "@types/simple-peer": "^9.11.1",
"@types/socket.io-client": "^1.4.32", "@types/socket.io-client": "^1.4.32",
"axios": "^0.21.1", "axios": "^0.21.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -51,7 +51,7 @@
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "1.3.6", "quill": "1.3.6",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"simple-peer": "^9.6.2", "simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4" "standardized-audio-context": "^25.2.4"
}, },
@ -61,7 +61,7 @@
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack", "build": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
"build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack", "build-typings": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production BUILD_TYPINGS=1 webpack",
"test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts", "lint": "node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
"precommit": "lint-staged", "precommit": "lint-staged",

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isDataLayerEvent = new tg.IsInterface()
.withProperties({
export const isDataLayerEvent = data: tg.isObject,
new tg.IsInterface().withProperties({ })
data: tg.isObject .get();
}).get();
/** /**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers * A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers

View File

@ -1,14 +1,15 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isGameStateEvent = export const isGameStateEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
roomId: tg.isString, roomId: tg.isString,
mapUrl: tg.isString, mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull), nickname: tg.isUnion(tg.isString, tg.isNull),
uuid: tg.isUnion(tg.isString, tg.isUndefined), uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull), startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags : tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame when the gameState is received by the script * A message sent from the game to the iFrame when the gameState is received by the script
*/ */

View File

@ -1,19 +1,17 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isHasPlayerMovedEvent = new tg.IsInterface()
.withProperties({
export const isHasPlayerMovedEvent = direction: tg.isElementOf("right", "left", "up", "down"),
new tg.IsInterface().withProperties({
direction: tg.isElementOf('right', 'left', 'up', 'down'),
moving: tg.isBoolean, moving: tg.isBoolean,
x: tg.isNumber, x: tg.isNumber,
y: tg.isNumber y: tg.isNumber,
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame to notify a movement from the current player. * A message sent from the game to the iFrame to notify a movement from the current player.
*/ */
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>; export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void

View File

@ -1,73 +1,73 @@
import type { GameStateEvent } from "./GameStateEvent";
import type { GameStateEvent } from './GameStateEvent'; import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ButtonClickedEvent } from './ButtonClickedEvent'; import type { ChatEvent } from "./ChatEvent";
import type { ChatEvent } from './ChatEvent'; import type { ClosePopupEvent } from "./ClosePopupEvent";
import type { ClosePopupEvent } from './ClosePopupEvent'; import type { EnterLeaveEvent } from "./EnterLeaveEvent";
import type { EnterLeaveEvent } from './EnterLeaveEvent'; import type { GoToPageEvent } from "./GoToPageEvent";
import type { GoToPageEvent } from './GoToPageEvent'; import type { LoadPageEvent } from "./LoadPageEvent";
import type { LoadPageEvent } from './LoadPageEvent'; import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent'; import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenPopupEvent } from './OpenPopupEvent'; import type { OpenTabEvent } from "./OpenTabEvent";
import type { OpenTabEvent } from './OpenTabEvent'; import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { UserInputChatEvent } from './UserInputChatEvent';
import type { DataLayerEvent } from "./DataLayerEvent"; import type { DataLayerEvent } from "./DataLayerEvent";
import type { LayerEvent } from './LayerEvent'; import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent"; import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent"; import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent'; import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent"; import type { SetTilesEvent } from "./SetTilesEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T data: T;
} }
/**
* List event types sent from an iFrame to WorkAdventure
*/
export type IframeEventMap = { export type IframeEventMap = {
//getState: GameStateEvent, loadPage: LoadPageEvent;
loadPage: LoadPageEvent chat: ChatEvent;
chat: ChatEvent, openPopup: OpenPopupEvent;
openPopup: OpenPopupEvent closePopup: ClosePopupEvent;
closePopup: ClosePopupEvent openTab: OpenTabEvent;
openTab: OpenTabEvent goToPage: GoToPageEvent;
goToPage: GoToPageEvent openCoWebSite: OpenCoWebSiteEvent;
openCoWebSite: OpenCoWebSiteEvent closeCoWebSite: null;
closeCoWebSite: null disablePlayerControls: null;
disablePlayerControls: null restorePlayerControls: null;
restorePlayerControls: null displayBubble: null;
displayBubble: null removeBubble: null;
removeBubble: null onPlayerMove: undefined;
onPlayerMove: undefined showLayer: LayerEvent;
showLayer: LayerEvent hideLayer: LayerEvent;
hideLayer: LayerEvent setProperty: SetPropertyEvent;
setProperty: SetPropertyEvent getDataLayer: undefined;
getDataLayer: undefined loadSound: LoadSoundEvent;
loadSound: LoadSoundEvent playSound: PlaySoundEvent;
playSound: PlaySoundEvent stopSound: null;
stopSound: null getState: undefined;
setTiles: SetTilesEvent registerMenuCommand: MenuItemRegisterEvent;
getState: undefined, setTiles: SetTilesEvent;
registerMenuCommand: MenuItemRegisterEvent };
}
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;
data: IframeEventMap[T]; data: IframeEventMap[T];
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string'; export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
typeof event.type === "string";
export interface IframeResponseEventMap { export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent userInputChat: UserInputChatEvent;
enterEvent: EnterLeaveEvent enterEvent: EnterLeaveEvent;
leaveEvent: EnterLeaveEvent leaveEvent: EnterLeaveEvent;
buttonClickedEvent: ButtonClickedEvent buttonClickedEvent: ButtonClickedEvent;
gameState: GameStateEvent hasPlayerMoved: HasPlayerMovedEvent;
hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent;
dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent;
menuItemClicked: MenuItemClickedEvent
} }
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> { export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T; type: T;
@ -75,4 +75,49 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string'; export const isIframeResponseEventWrapper = (event: {
type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
/**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
*/
export type IframeQueryMap = {
getState: {
query: undefined,
answer: GameStateEvent
},
}
export interface IframeQuery<T extends keyof IframeQueryMap> {
type: T;
data: IframeQueryMap[T]['query'];
}
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
id: number;
query: IframeQuery<T>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === 'string';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> => typeof event.id === 'number' && isIframeQuery(event.query);
export interface IframeAnswerEvent<T extends keyof IframeQueryMap> {
id: number;
type: T;
data: IframeQueryMap[T]['answer'];
}
export const isIframeAnswerEvent = (event: { type?: string, id?: number }): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === 'string' && typeof event.id === 'number';
export interface IframeErrorAnswerEvent {
id: number;
type: keyof IframeQueryMap;
error: string;
}
export const isIframeErrorAnswerEvent = (event: { type?: string, id?: number, error?: string }): event is IframeErrorAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number' && typeof event.error === 'string';

View File

@ -1,9 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isLayerEvent = export const isLayerEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
name: tg.isString, name: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to show/hide a layer. * A message sent from the iFrame to the game to show/hide a layer.
*/ */

View File

@ -1,11 +1,10 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isLoadPageEvent = new tg.IsInterface()
.withProperties({
export const isLoadPageEvent =
new tg.IsInterface().withProperties({
url: tg.isString, url: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.

View File

@ -1,13 +1,12 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isOpenCoWebsite = new tg.IsInterface()
.withProperties({
export const isOpenCoWebsite =
new tg.IsInterface().withProperties({
url: tg.isString, url: tg.isString,
allowApi: tg.isBoolean, allowApi: tg.isBoolean,
allowPolicy: tg.isString, allowPolicy: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a message in the chat. * A message sent from the iFrame to the game to add a message in the chat.

View File

@ -1,14 +1,15 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isSetTilesEvent = export const isSetTilesEvent = tg.isArray(
tg.isArray( new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
x: tg.isNumber, x: tg.isNumber,
y: tg.isNumber, y: tg.isNumber,
tile: tg.isUnion(tg.isNumber, tg.isString), tile: tg.isUnion(tg.isUnion(tg.isNumber, tg.isString), tg.isNull),
layer: tg.isString layer: tg.isString,
}).get() })
); .get()
);
/** /**
* A message sent from the iFrame to the game to set one or many tiles. * A message sent from the iFrame to the game to set one or many tiles.
*/ */

View File

@ -1,11 +1,12 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isSetPropertyEvent = export const isSetPropertyEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
layerName: tg.isString, layerName: tg.isString,
propertyName: tg.isString, propertyName: tg.isString,
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))) propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to change the value of the property of the layer * A message sent from the iFrame to the game to change the value of the property of the layer
*/ */

View File

@ -1,12 +1,11 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isMenuItemClickedEvent = export const isMenuItemClickedEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
menuItem: tg.isString menuItem: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the game to the iFrame when a menu item is clicked. * A message sent from the game to the iFrame when a menu item is clicked.
*/ */
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>; export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;

View File

@ -1,25 +1,26 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
import { Subject } from 'rxjs'; import { Subject } from "rxjs";
export const isMenuItemRegisterEvent = export const isMenuItemRegisterEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
menutItem: tg.isString menutItem: tg.isString,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to add a new menu item. * A message sent from the iFrame to the game to add a new menu item.
*/ */
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>; export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
export const isMenuItemRegisterIframeEvent = export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
type: tg.isSingletonString("registerMenuCommand"), type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent data: isMenuItemRegisterEvent,
}).get(); })
.get();
const _registerMenuCommandStream: Subject<string> = new Subject(); const _registerMenuCommandStream: Subject<string> = new Subject();
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable(); export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) { export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
_registerMenuCommandStream.next(event.menutItem) _registerMenuCommandStream.next(event.menutItem);
} }

View File

@ -1,42 +1,45 @@
import {Subject} from "rxjs"; import { Subject } from "rxjs";
import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import {HtmlUtils} from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent";
import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent";
import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
import {scriptUtils} from "./ScriptUtils"; import { scriptUtils } from "./ScriptUtils";
import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
import { import {
IframeErrorAnswerEvent,
IframeEvent, IframeEvent,
IframeEventMap, IframeEventMap, IframeQueryMap,
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap,
isIframeEventWrapper, isIframeEventWrapper,
TypedMessageEvent isIframeQueryWrapper,
TypedMessageEvent,
} from "./Events/IframeEvent"; } from "./Events/IframeEvent";
import type {UserInputChatEvent} from "./Events/UserInputChatEvent"; import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent"; import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
import type {DataLayerEvent} from "./Events/DataLayerEvent"; import type { DataLayerEvent } from "./Events/DataLayerEvent";
import type {GameStateEvent} from "./Events/GameStateEvent"; import type { GameStateEvent } from "./Events/GameStateEvent";
import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import {isLoadPageEvent} from "./Events/LoadPageEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent";
import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import {SetTilesEvent, isSetTilesEvent} from "./Events/SetTilesEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>;
/** /**
* Listens to messages from iframes and turn those messages into easy to use observables. * Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes. * Also allows to send messages to those iframes.
*/ */
class IframeListener { class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject(); private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable(); public readonly chatStream = this._chatStream.asObservable();
@ -82,9 +85,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject(); private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable(); public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _gameStateStream: Subject<void> = new Subject();
public readonly gameStateStream = this._gameStateStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject(); private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
@ -111,117 +111,147 @@ class IframeListener {
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false; private sendPlayerMove: boolean = false;
private answerers: {
[key in keyof IframeQueryMap]?: AnswererCallback<key>
} = {};
init() { init() {
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => { window.addEventListener(
// Do we trust the sender of this message? "message",
// Let's only accept messages from the iframe that are allowed. (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). // Do we trust the sender of this message?
let foundSrc: string | undefined; // Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined;
let iframe: HTMLIFrameElement; let iframe: HTMLIFrameElement | undefined;
for (iframe of this.iframes) { for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) { if (iframe.contentWindow === message.source) {
foundSrc = iframe.src; foundSrc = iframe.src;
break; break;
} }
}
const payload = message.data;
if (foundSrc === undefined) {
if (isIframeEventWrapper(payload)) {
console.warn('It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. ' +
'If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the ' +
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
'parameter to WA.nav.openCoWebSite())');
}
return;
}
foundSrc = this.getBaseUrl(foundSrc, message.source);
if (isIframeEventWrapper(payload)) {
if (payload.type === 'showLayer' && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
} else if (payload.type === 'hideLayer' && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data);
} else if (payload.type === 'setProperty' && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === 'chat' && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
}
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
}
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
}
else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
}
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
}
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
}
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
}
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url, foundSrc, payload.data.allowApi, payload.data.allowPolicy);
} }
else if (payload.type === 'closeCoWebSite') { const payload = message.data;
scriptUtils.closeCoWebSite();
if (foundSrc === undefined || iframe === undefined) {
if (isIframeEventWrapper(payload)) {
console.warn(
"It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " +
"If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the " +
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
"parameter to WA.nav.openCoWebSite())"
);
}
return;
} }
else if (payload.type === 'disablePlayerControls') { foundSrc = this.getBaseUrl(foundSrc, message.source);
this._disablePlayerControlStream.next();
}
else if (payload.type === 'restorePlayerControls') {
this._enablePlayerControlStream.next();
} else if (payload.type === 'displayBubble') {
this._displayBubbleStream.next();
} else if (payload.type === 'removeBubble') {
this._removeBubbleStream.next();
} else if (payload.type == "getState") {
this._gameStateStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
})
handleMenuItemRegistrationEvent(payload.data)
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
}
}
}, false);
if (isIframeQueryWrapper(payload)) {
const queryId = payload.id;
const query = payload.query;
const answerer = this.answerers[query.type];
if (answerer === undefined) {
const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.';
console.error(errorMsg);
iframe.contentWindow?.postMessage({
id: queryId,
type: query.type,
error: errorMsg
} as IframeErrorAnswerEvent, '*');
return;
}
Promise.resolve(answerer(query.data)).then((value) => {
iframe?.contentWindow?.postMessage({
id: queryId,
type: query.type,
data: value
}, '*');
}).catch(reason => {
console.error('An error occurred while responding to an iFrame query.', reason);
let reasonMsg: string;
if (reason instanceof Error) {
reasonMsg = reason.message;
} else {
reasonMsg = reason.toString();
}
iframe?.contentWindow?.postMessage({
id: queryId,
type: query.type,
error: reasonMsg
} as IframeErrorAnswerEvent, '*');
});
} else if (isIframeEventWrapper(payload)) {
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data);
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
this._setPropertyStream.next(payload.data);
} else if (payload.type === "chat" && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
} else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
} else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(
payload.data.url,
foundSrc,
payload.data.allowApi,
payload.data.allowPolicy
);
} else if (payload.type === "closeCoWebSite") {
scriptUtils.closeCoWebSite();
} else if (payload.type === "disablePlayerControls") {
this._disablePlayerControlStream.next();
} else if (payload.type === "restorePlayerControls") {
this._enablePlayerControlStream.next();
} else if (payload.type === "displayBubble") {
this._displayBubbleStream.next();
} else if (payload.type === "removeBubble") {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
});
handleMenuItemRegistrationEvent(payload.data);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
}
}
},
false
);
} }
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({ this.postMessage({
'type' : 'dataLayer', type: "dataLayer",
'data' : dataLayerEvent data: dataLayerEvent,
})
}
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
'type': 'gameState',
'data': gameStateEvent
}); });
} }
@ -234,25 +264,25 @@ class IframeListener {
} }
unregisterIframe(iframe: HTMLIFrameElement): void { unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframeCloseCallbacks.get(iframe)?.forEach(callback => { this.iframeCloseCallbacks.get(iframe)?.forEach((callback) => {
callback(); callback();
}); });
this.iframes.delete(iframe); this.iframes.delete(iframe);
} }
registerScript(scriptUrl: string): void { registerScript(scriptUrl: string): void {
console.log('Loading map related script at ', scriptUrl) console.log("Loading map related script at ", scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode ( // Using external iframe mode (
const iframe = document.createElement('iframe'); const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none'; iframe.style.display = "none";
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl); iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website. // We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts'); iframe.sandbox.add("allow-scripts");
iframe.sandbox.add('allow-top-navigation-by-user-activation'); iframe.sandbox.add("allow-top-navigation-by-user-activation");
document.body.prepend(iframe); document.body.prepend(iframe);
@ -260,45 +290,50 @@ class IframeListener {
this.registerIframe(iframe); this.registerIframe(iframe);
} else { } else {
// production code // production code
const iframe = document.createElement('iframe'); const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl); iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none'; iframe.style.display = "none";
// We are putting a sandbox on this script because it will run in the same domain as the main website. // We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts'); iframe.sandbox.add("allow-scripts");
iframe.sandbox.add('allow-top-navigation-by-user-activation'); iframe.sandbox.add("allow-top-navigation-by-user-activation");
//iframe.src = "data:text/html;charset=utf-8," + escape(html); //iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = '<!doctype html>\n' + iframe.srcdoc =
'\n' + "<!doctype html>\n" +
"\n" +
'<html lang="en">\n' + '<html lang="en">\n' +
'<head>\n' + "<head>\n" +
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' + '<script src="' +
'<script src="' + scriptUrl + '" ></script>\n' + window.location.protocol +
'<title></title>\n' + "//" +
'</head>\n' + window.location.host +
'</html>\n'; '/iframe_api.js" ></script>\n' +
'<script src="' +
scriptUrl +
'" ></script>\n' +
"<title></title>\n" +
"</head>\n" +
"</html>\n";
document.body.prepend(iframe); document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe); this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe); this.registerIframe(iframe);
} }
} }
private getBaseUrl(src: string, source: MessageEventSource | null): string{ private getBaseUrl(src: string, source: MessageEventSource | null): string {
for (const script of this.scripts) { for (const script of this.scripts) {
if (script[1].contentWindow === source) { if (script[1].contentWindow === source) {
return script[0]; return script[0];
} }
} }
return src; return src;
} }
private static getIFrameId(scriptUrl: string): string { private static getIFrameId(scriptUrl: string): string {
return 'script' + btoa(scriptUrl); return "script" + btoa(scriptUrl);
} }
unregisterScript(scriptUrl: string): void { unregisterScript(scriptUrl: string): void {
@ -315,47 +350,47 @@ class IframeListener {
sendUserInputChat(message: string) { sendUserInputChat(message: string) {
this.postMessage({ this.postMessage({
'type': 'userInputChat', type: "userInputChat",
'data': { data: {
'message': message, message: message,
} as UserInputChatEvent } as UserInputChatEvent,
}); });
} }
sendEnterEvent(name: string) { sendEnterEvent(name: string) {
this.postMessage({ this.postMessage({
'type': 'enterEvent', type: "enterEvent",
'data': { data: {
"name": name name: name,
} as EnterLeaveEvent } as EnterLeaveEvent,
}); });
} }
sendLeaveEvent(name: string) { sendLeaveEvent(name: string) {
this.postMessage({ this.postMessage({
'type': 'leaveEvent', type: "leaveEvent",
'data': { data: {
"name": name name: name,
} as EnterLeaveEvent } as EnterLeaveEvent,
}); });
} }
hasPlayerMoved(event: HasPlayerMovedEvent) { hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) { if (this.sendPlayerMove) {
this.postMessage({ this.postMessage({
'type': 'hasPlayerMoved', type: "hasPlayerMoved",
'data': event data: event,
}); });
} }
} }
sendButtonClickedEvent(popupId: number, buttonId: number): void { sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({ this.postMessage({
'type': 'buttonClickedEvent', type: "buttonClickedEvent",
'data': { data: {
popupId, popupId,
buttonId buttonId,
} as ButtonClickedEvent } as ButtonClickedEvent,
}); });
} }
@ -364,10 +399,25 @@ class IframeListener {
*/ */
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) { public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
for (const iframe of this.iframes) { for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*'); iframe.contentWindow?.postMessage(message, "*");
} }
} }
/**
* Registers a callback that can be used to respond to some query (as defined in the IframeQueryMap type).
*
* Important! There can be only one "answerer" so registering a new one will unregister the old one.
*
* @param key The "type" of the query we are answering
* @param callback
*/
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']> ): void {
this.answerers[key] = callback;
}
public unregisterAnswerer(key: keyof IframeQueryMap): void {
delete this.answerers[key];
}
} }
export const iframeListener = new IframeListener(); export const iframeListener = new IframeListener();

View File

@ -1,21 +1,19 @@
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../WebRtc/CoWebsiteManager";
class ScriptUtils { class ScriptUtils {
public openTab(url: string) {
public openTab(url : string){
window.open(url); window.open(url);
} }
public goToPage(url : string){ public goToPage(url: string) {
window.location.href = url; window.location.href = url;
} }
public openCoWebsite(url: string, base: string, api: boolean, policy: string) { public openCoWebsite(url: string, base: string, api: boolean, policy: string) {
coWebsiteManager.loadCoWebsite(url, base, api, policy); coWebsiteManager.loadCoWebsite(url, base, api, policy);
} }
public closeCoWebSite(){ public closeCoWebSite() {
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
} }
} }

View File

@ -1,9 +1,40 @@
import type * as tg from "generic-type-guard"; import type * as tg from "generic-type-guard";
import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent'; import type {
IframeEvent,
IframeEventMap, IframeQuery,
IframeQueryMap,
IframeResponseEventMap
} from '../Events/IframeEvent';
import type {IframeQueryWrapper} from "../Events/IframeEvent";
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) { export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
window.parent.postMessage(content, "*") window.parent.postMessage(content, "*")
} }
let queryNumber = 0;
export const answerPromises = new Map<number, {
resolve: (value: (IframeQueryMap[keyof IframeQueryMap]['answer'] | PromiseLike<IframeQueryMap[keyof IframeQueryMap]['answer']>)) => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason?: any) => void
}>();
export function queryWorkadventure<T extends keyof IframeQueryMap>(content: IframeQuery<T>): Promise<IframeQueryMap[T]['answer']> {
return new Promise<IframeQueryMap[T]['answer']>((resolve, reject) => {
window.parent.postMessage({
id: queryNumber,
query: content
} as IframeQueryWrapper<T>, "*");
answerPromises.set(queryNumber, {
resolve,
reject
});
queryNumber++;
});
}
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> { export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {

View File

@ -1,11 +1,11 @@
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent'; import type { MenuItemClickedEvent } from "../../Events/ui/MenuItemClickedEvent";
import { iframeListener } from '../../IframeListener'; import { iframeListener } from "../../IframeListener";
export function sendMenuClickedEvent(menuItem: string) { export function sendMenuClickedEvent(menuItem: string) {
iframeListener.postMessage({ iframeListener.postMessage({
'type': 'menuItemClicked', type: "menuItemClicked",
'data': { data: {
menuItem: menuItem, menuItem: menuItem,
} as MenuItemClickedEvent } as MenuItemClickedEvent,
}); });
} }

View File

@ -2,8 +2,15 @@ import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribut
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import { getGameState } from "./room";
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent"; import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
interface User {
id: string | undefined;
nickName: string | null;
tags: string[];
}
const moveStream = new Subject<HasPlayerMovedEvent>(); const moveStream = new Subject<HasPlayerMovedEvent>();
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> { export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
@ -24,6 +31,11 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
data: null, data: null,
}); });
} }
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
});
}
} }
export default new WorkadventurePlayerCommands(); export default new WorkadventurePlayerCommands();

View File

@ -4,7 +4,7 @@ import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { isGameStateEvent } from "../Events/GameStateEvent"; import { isGameStateEvent } from "../Events/GameStateEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
@ -14,7 +14,6 @@ import type { GameStateEvent } from "../Events/GameStateEvent";
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>(); const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>(); const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const dataLayerResolver = new Subject<DataLayerEvent>(); const dataLayerResolver = new Subject<DataLayerEvent>();
const stateResolvers = new Subject<GameStateEvent>();
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined; let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
@ -25,26 +24,16 @@ interface Room {
startLayer: string | null; startLayer: string | null;
} }
interface User {
id: string | undefined;
nickName: string | null;
tags: string[];
}
interface TileDescriptor { interface TileDescriptor {
x: number x: number;
y: number y: number;
tile: number | string tile: number | string | null;
layer: string layer: string;
} }
export function getGameState(): Promise<GameStateEvent> {
function getGameState(): Promise<GameStateEvent> {
if (immutableDataPromise === undefined) { if (immutableDataPromise === undefined) {
immutableDataPromise = new Promise<GameStateEvent>((resolver, thrower) => { immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
stateResolvers.subscribe(resolver);
sendToWorkadventure({ type: "getState", data: null });
});
} }
return immutableDataPromise; return immutableDataPromise;
} }
@ -72,13 +61,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
leaveStreams.get(payloadData.name)?.next(); leaveStreams.get(payloadData.name)?.next();
}, },
}), }),
apiCallback({
type: "gameState",
typeChecker: isGameStateEvent,
callback: (payloadData) => {
stateResolvers.next(payloadData);
},
}),
apiCallback({ apiCallback({
type: "dataLayer", type: "dataLayer",
typeChecker: isDataLayerEvent, typeChecker: isDataLayerEvent,
@ -132,18 +114,12 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
}); });
}); });
} }
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
});
}
setTiles(tiles: TileDescriptor[]) { setTiles(tiles: TileDescriptor[]) {
sendToWorkadventure({ sendToWorkadventure({
type: 'setTiles', type: "setTiles",
data: tiles data: tiles,
}) });
} }
} }
export default new WorkadventureRoomCommands(); export default new WorkadventureRoomCommands();

View File

@ -10,12 +10,14 @@
import {errorStore} from "../Stores/ErrorStore"; import {errorStore} from "../Stores/ErrorStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte"; import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte"; import LoginScene from "./Login/LoginScene.svelte";
import Chat from "./Chat/Chat.svelte";
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore"; import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte"; import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte"; import VisitCard from "./VisitCard/VisitCard.svelte";
import {requestVisitCardsStore} from "../Stores/GameStore"; import {requestVisitCardsStore} from "../Stores/GameStore";
import type {Game} from "../Phaser/Game/Game"; import type {Game} from "../Phaser/Game/Game";
import {chatVisibilityStore} from "../Stores/ChatStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore"; import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte"; import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte"; import AudioPlaying from "./UI/AudioPlaying.svelte";
@ -61,14 +63,6 @@
<AudioPlaying url={$soundPlayingStore} /> <AudioPlaying url={$soundPlayingStore} />
</div> </div>
{/if} {/if}
<!--
{#if $menuIconVisible}
<div>
<MenuIcon />
</div>
{/if}
-->
{#if $gameOverlayVisibilityStore} {#if $gameOverlayVisibilityStore}
<div> <div>
<VideoOverlay></VideoOverlay> <VideoOverlay></VideoOverlay>
@ -94,4 +88,7 @@
<ErrorDialog></ErrorDialog> <ErrorDialog></ErrorDialog>
</div> </div>
{/if} {/if}
{#if $chatVisibilityStore}
<Chat></Chat>
{/if}
</div> </div>

View File

@ -0,0 +1,101 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
import ChatMessageForm from './ChatMessageForm.svelte';
import ChatElement from './ChatElement.svelte';
import { afterUpdate, beforeUpdate } from "svelte";
let listDom: HTMLElement;
let autoscroll: boolean;
beforeUpdate(() => {
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20);
});
afterUpdate(() => {
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
});
function closeChat() {
chatVisibilityStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeChat();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}">
<p class="close-icon" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}>
<ul>
<li><p class="system-text">Here is your chat history: </p></li>
{#each $chatMessagesStore as message, i}
<li><ChatElement message={message} line={i}></ChatElement></li>
{/each}
</ul>
</section>
<section class="messageForm">
<ChatMessageForm></ChatMessageForm>
</section>
</aside>
<style lang="scss">
p.close-icon {
position: absolute;
padding: 4px;
right: 12px;
font-size: 30px;
line-height: 25px;
cursor: pointer;
}
p.system-text {
border-radius: 8px;
margin-bottom: 10px;
padding:6px;
overflow-wrap: break-word;
max-width: 100%;
background: gray;
display: inline-block;
}
aside.chatWindow {
z-index:100;
pointer-events: auto;
position: absolute;
top: 0;
left: 0;
height: 100vh;
width:30vw;
min-width: 350px;
background: rgb(5, 31, 51, 0.9);
color: whitesmoke;
display: flex;
flex-direction: column;
padding: 10px;
border-bottom-right-radius: 16px;
border-top-right-radius: 16px;
.messagesList {
margin-top: 35px;
overflow-y: auto;
flex: auto;
ul {
list-style-type: none;
padding-left: 0;
}
}
.messageForm {
flex: 0 70px;
padding-top: 15px;
}
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts">
import {ChatMessageTypes} from "../../Stores/ChatStore";
import type {ChatMessage} from "../../Stores/ChatStore";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import ChatPlayerName from './ChatPlayerName.svelte';
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
export let message: ChatMessage;
export let line: number;
$: author = message.author as PlayerInterface;
$: targets = message.targets || [];
$: texts = message.text || [];
function urlifyText(text: string): string {
return HtmlUtils.urlify(text)
}
function renderDate(date: Date) {
return date.toLocaleTimeString(navigator.language, {
hour: '2-digit',
minute:'2-digit'
});
}
function isLastIteration(index: number) {
return targets.length -1 === index;
}
</script>
<div class="chatElement">
<div class="messagePart">
{#if message.type === ChatMessageTypes.userIncoming}
&gt;&gt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.userOutcoming}
&lt;&lt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.me}
<h4>Me: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text}
<div><p class="my-text">{@html urlifyText(text)}</p></div>
{/each}
{:else}
<h4><ChatPlayerName player={author} line={line}></ChatPlayerName>: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text}
<div><p class="other-text">{@html urlifyText(text)}</p></div>
{/each}
{/if}
</div>
</div>
<style lang="scss">
h4, p {
font-family: Lato;
}
div.chatElement {
display: flex;
margin-bottom: 20px;
.messagePart {
flex-grow:1;
max-width: 100%;
span.date {
font-size: 80%;
color: gray;
}
div > p {
border-radius: 8px;
margin-bottom: 10px;
padding:6px;
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
&.other-text {
background: gray;
}
&.my-text {
background: #6489ff;
}
}
}
}
</style>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
let newMessageText = '';
function onFocus() {
chatInputFocusStore.set(true);
}
function onBlur() {
chatInputFocusStore.set(false);
}
function saveMessage() {
if (!newMessageText) return;
chatMessagesStore.addPersonnalMessage(newMessageText);
newMessageText = '';
}
</script>
<form on:submit|preventDefault={saveMessage}>
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} >
<button type="submit">
<img src="/static/images/send.png" alt="Send" width="20">
</button>
</form>
<style lang="scss">
form {
display: flex;
padding-left: 4px;
padding-right: 4px;
input {
flex: auto;
background-color: #254560;
color: white;
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
border: none;
font-size: 22px;
font-family: Lato;
padding-left: 6px;
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
outline: none;
}
button {
background-color: #254560;
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
border: none;
border-left: solid white 1px;
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,51 @@
<script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import {chatSubMenuVisbilityStore} from "../../Stores/ChatStore";
import {onDestroy, onMount} from "svelte";
import type {Unsubscriber} from "svelte/store";
import ChatSubMenu from "./ChatSubMenu.svelte";
export let player: PlayerInterface;
export let line: number;
let isSubMenuOpen: boolean;
let chatSubMenuVisivilytUnsubcribe: Unsubscriber;
function openSubMenu() {
chatSubMenuVisbilityStore.openSubMenu(player.name, line);
}
onMount(() => {
chatSubMenuVisivilytUnsubcribe = chatSubMenuVisbilityStore.subscribe((newValue) => {
isSubMenuOpen = (newValue === player.name + line);
})
})
onDestroy(() => {
chatSubMenuVisivilytUnsubcribe();
})
</script>
<span class="subMenu">
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}>
{player.name}
</span>
{#if isSubMenuOpen}
<ChatSubMenu player={player}/>
{/if}
</span>
<style lang="scss">
span.subMenu {
display: inline-block;
}
span.chatPlayerName {
margin-left: 3px;
}
.chatPlayerName:hover {
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import {requestVisitCardsStore} from "../../Stores/GameStore";
export let player: PlayerInterface;
function openVisitCard() {
if (player.visitCardUrl) {
requestVisitCardsStore.set(player.visitCardUrl);
}
}
</script>
<ul class="selectMenu" style="border-top: {player.color || 'whitesmoke'} 5px solid">
<li><button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}>Visit card</button></li>
<li><button class="text-btn" disabled>Add friend</button></li>
</ul>
<style lang="scss">
ul.selectMenu {
background-color: whitesmoke;
position: absolute;
padding: 5px;
border-radius: 4px;
list-style-type: none;
li {
text-align: center;
}
}
</style>

View File

@ -37,9 +37,7 @@
<img alt="Report this user" src={reportImg}> <img alt="Report this user" src={reportImg}>
<span>Report/Block</span> <span>Report/Block</span>
</button> </button>
{#if $streamStore }
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video> <video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
{/if}
<img src={blockSignImg} class="block-logo" alt="Block" /> <img src={blockSignImg} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false} {#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget> <SoundMeterWidget stream={$streamStore}></SoundMeterWidget>

View File

@ -1,4 +1,7 @@
export function getColorByString(str: string) : string|null { import type { UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../../Enum/EnvironmentVariable";
export function getColorByString(str: string): string | null {
let hash = 0; let hash = 0;
if (str.length === 0) { if (str.length === 0) {
return null; return null;
@ -7,21 +10,37 @@ export function getColorByString(str: string) : string|null {
hash = str.charCodeAt(i) + ((hash << 5) - hash); hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; hash = hash & hash;
} }
let color = '#'; let color = "#";
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 255; const value = (hash >> (i * 8)) & 255;
color += ('00' + value.toString(16)).substr(-2); color += ("00" + value.toString(16)).substr(-2);
} }
return color; return color;
} }
export function srcObject(node: HTMLVideoElement, stream: MediaStream) { export function srcObject(node: HTMLVideoElement, stream: MediaStream | null) {
node.srcObject = stream; node.srcObject = stream;
return { return {
update(newStream: MediaStream) { update(newStream: MediaStream) {
if (node.srcObject != newStream) { if (node.srcObject != newStream) {
node.srcObject = newStream node.srcObject = newStream;
} }
} },
} };
}
export function getIceServersConfig(user: UserSimplePeerInterface): RTCIceServer[] {
const config: RTCIceServer[] = [
{
urls: STUN_SERVER.split(","),
},
];
if (TURN_SERVER !== "") {
config.push({
urls: TURN_SERVER.split(","),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD,
});
}
return config;
} }

View File

@ -45,8 +45,9 @@
.visitCard { .visitCard {
pointer-events: all; pointer-events: all;
margin-left: auto; position: absolute;
margin-right: auto; left: 50%;
transform: translate(-50%, 0);
margin-top: 200px; margin-top: 200px;
max-width: 80vw; max-width: 80vw;

View File

@ -38,11 +38,9 @@ class ConnectionManager {
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures); this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
const organizationSlug = data.organizationSlug; const roomUrl = data.roomUrl;
const worldSlug = data.worldSlug;
const roomSlug = data.roomSlug;
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash); const room = await Room.createRoom(new URL(window.location.protocol + '//' + window.location.host + roomUrl + window.location.search + window.location.hash));
urlManager.pushRoomIdToUrl(room); urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room); return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) { } else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
@ -66,22 +64,21 @@ class ConnectionManager {
throw "Error to store local user data"; throw "Error to store local user data";
} }
let roomId: string; let roomPath: string;
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
roomId = START_ROOM_URL; roomPath = window.location.protocol + '//' + window.location.host + START_ROOM_URL;
} else { } else {
roomId = window.location.pathname + window.location.search + window.location.hash; roomPath = window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search + window.location.hash;
} }
//get detail map for anonymous login and set texture in local storage //get detail map for anonymous login and set texture in local storage
const room = new Room(roomId); const room = await Room.createRoom(new URL(roomPath));
const mapDetail = await room.getMapDetail(); if(room.textures != undefined && room.textures.length > 0) {
if(mapDetail.textures != undefined && mapDetail.textures.length > 0) {
//check if texture was changed //check if texture was changed
if(localUser.textures.length === 0){ if(localUser.textures.length === 0){
localUser.textures = mapDetail.textures; localUser.textures = room.textures;
}else{ }else{
mapDetail.textures.forEach((newTexture) => { room.textures.forEach((newTexture) => {
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id); const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){ if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
return; return;
@ -114,9 +111,9 @@ class ConnectionManager {
this.localUser = new LocalUser('', 'test', []); this.localUser = new LocalUser('', 'test', []);
} }
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> { public connectToRoomSocket(roomUrl: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => { return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion); const connection = new RoomConnection(this.localUser.jwtToken, roomUrl, name, characterLayers, position, viewport, companion);
connection.onConnectError((error: object) => { connection.onConnectError((error: object) => {
console.log('An error occurred while connecting to socket server. Retrying'); console.log('An error occurred while connecting to socket server. Retrying');
reject(error); reject(error);
@ -137,7 +134,7 @@ class ConnectionManager {
this.reconnectingTimeout = setTimeout(() => { this.reconnectingTimeout = setTimeout(() => {
//todo: allow a way to break recursion? //todo: allow a way to break recursion?
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely. //todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection)); this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
}, 4000 + Math.floor(Math.random() * 2000) ); }, 4000 + Math.floor(Math.random() * 2000) );
}); });
}); });

View File

@ -1,8 +1,8 @@
import type {SignalData} from "simple-peer"; import type { SignalData } from "simple-peer";
import type {RoomConnection} from "./RoomConnection"; import type { RoomConnection } from "./RoomConnection";
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
export enum EventMessage{ export enum EventMessage {
CONNECT = "connect", CONNECT = "connect",
WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal", WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
@ -17,7 +17,7 @@ export enum EventMessage{
GROUP_CREATE_UPDATE = "group-create-update", GROUP_CREATE_UPDATE = "group-create-update",
GROUP_DELETE = "group-delete", GROUP_DELETE = "group-delete",
SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id. SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id.
ITEM_EVENT = 'item-event', ITEM_EVENT = "item-event",
CONNECT_ERROR = "connect_error", CONNECT_ERROR = "connect_error",
CONNECTING_ERROR = "connecting_error", CONNECTING_ERROR = "connecting_error",
@ -36,7 +36,7 @@ export enum EventMessage{
export interface PointInterface { export interface PointInterface {
x: number; x: number;
y: number; y: number;
direction : string; direction: string;
moving: boolean; moving: boolean;
} }
@ -45,8 +45,9 @@ export interface MessageUserPositionInterface {
name: string; name: string;
characterLayers: BodyResourceDescriptionInterface[]; characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
visitCardUrl: string|null; visitCardUrl: string | null;
companion: string|null; companion: string | null;
userUuid: string;
} }
export interface MessageUserMovedInterface { export interface MessageUserMovedInterface {
@ -60,58 +61,59 @@ export interface MessageUserJoined {
characterLayers: BodyResourceDescriptionInterface[]; characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
visitCardUrl: string | null; visitCardUrl: string | null;
companion: string|null; companion: string | null;
userUuid: string;
} }
export interface PositionInterface { export interface PositionInterface {
x: number, x: number;
y: number y: number;
} }
export interface GroupCreatedUpdatedMessageInterface { export interface GroupCreatedUpdatedMessageInterface {
position: PositionInterface, position: PositionInterface;
groupId: number, groupId: number;
groupSize: number groupSize: number;
} }
export interface WebRtcDisconnectMessageInterface { export interface WebRtcDisconnectMessageInterface {
userId: number userId: number;
} }
export interface WebRtcSignalReceivedMessageInterface { export interface WebRtcSignalReceivedMessageInterface {
userId: number, userId: number;
signal: SignalData, signal: SignalData;
webRtcUser: string | undefined, webRtcUser: string | undefined;
webRtcPassword: string | undefined webRtcPassword: string | undefined;
} }
export interface ViewportInterface { export interface ViewportInterface {
left: number, left: number;
top: number, top: number;
right: number, right: number;
bottom: number, bottom: number;
} }
export interface ItemEventMessageInterface { export interface ItemEventMessageInterface {
itemId: number, itemId: number;
event: string, event: string;
state: unknown, state: unknown;
parameters: unknown parameters: unknown;
} }
export interface RoomJoinedMessageInterface { export interface RoomJoinedMessageInterface {
//users: MessageUserPositionInterface[], //users: MessageUserPositionInterface[],
//groups: GroupCreatedUpdatedMessageInterface[], //groups: GroupCreatedUpdatedMessageInterface[],
items: { [itemId: number] : unknown } items: { [itemId: number]: unknown };
} }
export interface PlayGlobalMessageInterface { export interface PlayGlobalMessageInterface {
id: string id: string;
type: string type: string;
message: string message: string;
} }
export interface OnConnectInterface { export interface OnConnectInterface {
connection: RoomConnection, connection: RoomConnection;
room: RoomJoinedMessageInterface room: RoomJoinedMessageInterface;
} }

View File

@ -1,90 +1,105 @@
import Axios from "axios"; import Axios from "axios";
import {PUSHER_URL} from "../Enum/EnvironmentVariable"; import { PUSHER_URL } from "../Enum/EnvironmentVariable";
import type {CharacterTexture} from "./LocalUser"; import type { CharacterTexture } from "./LocalUser";
export class MapDetail{ export class MapDetail {
constructor(public readonly mapUrl: string, public readonly textures : CharacterTexture[]|undefined) { constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
} }
export interface RoomRedirect {
redirectUrl: string;
} }
export class Room { export class Room {
public readonly id: string; public readonly id: string;
public readonly isPublic: boolean; public readonly isPublic: boolean;
private mapUrl: string|undefined; private _mapUrl: string | undefined;
private textures: CharacterTexture[]|undefined; private _textures: CharacterTexture[] | undefined;
private instance: string|undefined; private instance: string | undefined;
private _search: URLSearchParams; private readonly _search: URLSearchParams;
constructor(id: string) { private constructor(private roomUrl: URL) {
const url = new URL(id, 'https://example.com'); this.id = roomUrl.pathname;
this.id = url.pathname; if (this.id.startsWith("/")) {
if (this.id.startsWith('/')) {
this.id = this.id.substr(1); this.id = this.id.substr(1);
} }
if (this.id.startsWith('_/')) { if (this.id.startsWith("_/")) {
this.isPublic = true; this.isPublic = true;
} else if (this.id.startsWith('@/')) { } else if (this.id.startsWith("@/")) {
this.isPublic = false; this.isPublic = false;
} else { } else {
throw new Error('Invalid room ID'); throw new Error("Invalid room ID");
} }
this._search = new URLSearchParams(url.search); this._search = new URLSearchParams(roomUrl.search);
} }
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} { /**
let roomId = ''; * Creates a "Room" object representing the room.
let hash = ''; * This method will follow room redirects if necessary, so the instance returned is a "real" room.
if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link */
//Relative identifier can be deep enough to rewrite the base domain, so we cannot use the variable 'baseUrl' as the actual base url for the URL objects. public static async createRoom(roomUrl: URL): Promise<Room> {
//We instead use 'workadventure' as a dummy base value. let redirectCount = 0;
const baseUrlObject = new URL(baseUrl); while (redirectCount < 32) {
const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/'+currentInstance+'/'+baseUrlObject.hostname+baseUrlObject.pathname); const room = new Room(roomUrl);
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId const result = await room.getMapDetail();
roomId = roomId.substring(1); //remove the leading slash if (result instanceof MapDetail) {
hash = absoluteExitSceneUrl.hash; return room;
hash = hash.substring(1); //remove the leading diese
} else { //absolute room Id
const parts = identifier.split('#');
roomId = parts[0];
roomId = roomId.substring(1); //remove the leading slash
if (parts.length > 1) {
hash = parts[1]
} }
redirectCount++;
roomUrl = new URL(result.redirectUrl);
} }
return {roomId, hash} throw new Error("Room resolving seems stuck in a redirect loop after 32 redirect attempts");
} }
public async getMapDetail(): Promise<MapDetail> { public static getRoomPathFromExitUrl(exitUrl: string, currentRoomUrl: string): URL {
return new Promise<MapDetail>((resolve, reject) => { const url = new URL(exitUrl, currentRoomUrl);
if (this.mapUrl !== undefined && this.textures != undefined) { return url;
resolve(new MapDetail(this.mapUrl, this.textures)); }
return;
}
if (this.isPublic) { /**
const match = /_\/[^/]+\/(.+)/.exec(this.id); * @deprecated USage of exitSceneUrl is deprecated and therefore, this method is deprecated too.
if (!match) throw new Error('Could not extract url from "'+this.id+'"'); */
this.mapUrl = window.location.protocol+'//'+match[1]; public static getRoomPathFromExitSceneUrl(
resolve(new MapDetail(this.mapUrl, this.textures)); exitSceneUrl: string,
return; currentRoomUrl: string,
} else { currentMapUrl: string
// We have a private ID, we need to query the map URL from the server. ): URL {
const urlParts = this.parsePrivateUrl(this.id); const absoluteExitSceneUrl = new URL(exitSceneUrl, currentMapUrl);
const baseUrl = new URL(currentRoomUrl);
Axios.get(`${PUSHER_URL}/map`, { const currentRoom = new Room(baseUrl);
params: urlParts let instance: string = "global";
}).then(({data}) => { if (currentRoom.isPublic) {
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl); instance = currentRoom.instance as string;
resolve(data); }
return;
}).catch((reason) => { baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
reject(reason); if (absoluteExitSceneUrl.hash) {
}); baseUrl.hash = absoluteExitSceneUrl.hash;
} }
return baseUrl;
}
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
const result = await Axios.get(`${PUSHER_URL}/map`, {
params: {
playUri: this.roomUrl.toString(),
},
}); });
const data = result.data;
if (data.redirectUrl) {
return {
redirectUrl: data.redirectUrl as string,
};
}
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl;
this._textures = data.textures;
return new MapDetail(data.mapUrl, data.textures);
} }
/** /**
@ -99,37 +114,39 @@ export class Room {
if (this.isPublic) { if (this.isPublic) {
const match = /_\/([^/]+)\/.+/.exec(this.id); const match = /_\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "'+this.id+'"'); if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1]; this.instance = match[1];
return this.instance; return this.instance;
} else { } else {
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id); const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "'+this.id+'"'); if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1]+'/'+match[2]; this.instance = match[1] + "/" + match[2];
return this.instance; return this.instance;
} }
} }
private parsePrivateUrl(url: string): { organizationSlug: string, worldSlug: string, roomSlug?: string } { /**
* @deprecated
*/
private parsePrivateUrl(url: string): { organizationSlug: string; worldSlug: string; roomSlug?: string } {
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm; const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
const match = regex.exec(url); const match = regex.exec(url);
if (!match) { if (!match) {
throw new Error('Invalid URL '+url); throw new Error("Invalid URL " + url);
} }
const results: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { const results: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
organizationSlug: match[1], organizationSlug: match[1],
worldSlug: match[2], worldSlug: match[2],
} };
if (match[3] !== undefined) { if (match[3] !== undefined) {
results.roomSlug = match[3]; results.roomSlug = match[3];
} }
return results; return results;
} }
public isDisconnected(): boolean public isDisconnected(): boolean {
{ const alone = this._search.get("alone");
const alone = this._search.get('alone'); if (alone && alone !== "0" && alone.toLowerCase() !== "false") {
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
return true; return true;
} }
return false; return false;
@ -138,4 +155,33 @@ export class Room {
public get search(): URLSearchParams { public get search(): URLSearchParams {
return this._search; return this._search;
} }
/**
* 2 rooms are equal if they share the same path (but not necessarily the same hash)
* @param room
*/
public isEqual(room: Room): boolean {
return room.key === this.key;
}
/**
* A key representing this room
*/
public get key(): string {
const newUrl = new URL(this.roomUrl.toString());
newUrl.search = "";
newUrl.hash = "";
return newUrl.toString();
}
get textures(): CharacterTexture[] | undefined {
return this._textures;
}
get mapUrl(): string {
if (!this._mapUrl) {
throw new Error("Map URL not fetched yet");
}
return this._mapUrl;
}
} }

View File

@ -11,7 +11,8 @@ import {
RoomJoinedMessage, RoomJoinedMessage,
ServerToClientMessage, ServerToClientMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SilentMessage, StopGlobalMessage, SilentMessage,
StopGlobalMessage,
UserJoinedMessage, UserJoinedMessage,
UserLeftMessage, UserLeftMessage,
UserMovedMessage, UserMovedMessage,
@ -31,17 +32,22 @@ import {
EmotePromptMessage, EmotePromptMessage,
SendUserMessage, SendUserMessage,
BanUserMessage, BanUserMessage,
} from "../Messages/generated/messages_pb" } from "../Messages/generated/messages_pb";
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils"; import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
import { import {
EventMessage, EventMessage,
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface, GroupCreatedUpdatedMessageInterface,
MessageUserJoined, OnConnectInterface, PlayGlobalMessageInterface, PositionInterface, ItemEventMessageInterface,
MessageUserJoined,
OnConnectInterface,
PlayGlobalMessageInterface,
PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
ViewportInterface, WebRtcDisconnectMessageInterface, ViewportInterface,
WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels"; } from "./ConnexionModels";
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
@ -61,36 +67,45 @@ export class RoomConnection implements RoomConnection {
private closed: boolean = false; private closed: boolean = false;
private tags: string[] = []; private tags: string[] = [];
public static setWebsocketFactory(websocketFactory: (url: string) => any): void { // eslint-disable-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
RoomConnection.websocketFactory = websocketFactory; RoomConnection.websocketFactory = websocketFactory;
} }
/** /**
* *
* @param token A JWT token containing the UUID of the user * @param token A JWT token containing the UUID of the user
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" * @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
*/ */
public constructor(token: string | null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string | null) { public constructor(
token: string | null,
roomUrl: string,
name: string,
characterLayers: string[],
position: PositionInterface,
viewport: ViewportInterface,
companion: string | null
) {
let url = new URL(PUSHER_URL, window.location.toString()).toString(); let url = new URL(PUSHER_URL, window.location.toString()).toString();
url = url.replace('http://', 'ws://').replace('https://', 'wss://'); url = url.replace("http://", "ws://").replace("https://", "wss://");
if (!url.endsWith('/')) { if (!url.endsWith("/")) {
url += '/'; url += "/";
} }
url += 'room'; url += "room";
url += '?roomId=' + (roomId ? encodeURIComponent(roomId) : ''); url += "?roomId=" + encodeURIComponent(roomUrl);
url += '&token=' + (token ? encodeURIComponent(token) : ''); url += "&token=" + (token ? encodeURIComponent(token) : "");
url += '&name=' + encodeURIComponent(name); url += "&name=" + encodeURIComponent(name);
for (const layer of characterLayers) { for (const layer of characterLayers) {
url += '&characterLayers=' + encodeURIComponent(layer); url += "&characterLayers=" + encodeURIComponent(layer);
} }
url += '&x=' + Math.floor(position.x); url += "&x=" + Math.floor(position.x);
url += '&y=' + Math.floor(position.y); url += "&y=" + Math.floor(position.y);
url += '&top=' + Math.floor(viewport.top); url += "&top=" + Math.floor(viewport.top);
url += '&bottom=' + Math.floor(viewport.bottom); url += "&bottom=" + Math.floor(viewport.bottom);
url += '&left=' + Math.floor(viewport.left); url += "&left=" + Math.floor(viewport.left);
url += '&right=' + Math.floor(viewport.right); url += "&right=" + Math.floor(viewport.right);
if (typeof companion === 'string') { if (typeof companion === "string") {
url += '&companion=' + encodeURIComponent(companion); url += "&companion=" + encodeURIComponent(companion);
} }
if (RoomConnection.websocketFactory) { if (RoomConnection.websocketFactory) {
@ -99,7 +114,7 @@ export class RoomConnection implements RoomConnection {
this.socket = new WebSocket(url); this.socket = new WebSocket(url);
} }
this.socket.binaryType = 'arraybuffer'; this.socket.binaryType = "arraybuffer";
let interval: ReturnType<typeof setInterval> | undefined = undefined; let interval: ReturnType<typeof setInterval> | undefined = undefined;
@ -109,7 +124,7 @@ export class RoomConnection implements RoomConnection {
interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
}; };
this.socket.addEventListener('close', (event) => { this.socket.addEventListener("close", (event) => {
if (interval) { if (interval) {
clearInterval(interval); clearInterval(interval);
} }
@ -126,7 +141,7 @@ export class RoomConnection implements RoomConnection {
if (message.hasBatchmessage()) { if (message.hasBatchmessage()) {
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) { for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
let event: string|null = null; let event: string | null = null;
let payload; let payload;
if (subMessage.hasUsermovedmessage()) { if (subMessage.hasUsermovedmessage()) {
event = EventMessage.USER_MOVED; event = EventMessage.USER_MOVED;
@ -150,7 +165,7 @@ export class RoomConnection implements RoomConnection {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage; const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote()); emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else { } else {
throw new Error('Unexpected batch message type'); throw new Error("Unexpected batch message type");
} }
if (event) { if (event) {
@ -171,8 +186,8 @@ export class RoomConnection implements RoomConnection {
this.dispatch(EventMessage.CONNECT, { this.dispatch(EventMessage.CONNECT, {
connection: this, connection: this,
room: { room: {
items items,
} as RoomJoinedMessageInterface } as RoomJoinedMessageInterface,
}); });
} else if (message.hasWorldfullmessage()) { } else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage(); worldFullMessageStream.onMessage();
@ -183,7 +198,10 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasWebrtcsignaltoclientmessage()) { } else if (message.hasWebrtcsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) { } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage()); this.dispatch(
EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL,
message.getWebrtcscreensharingsignaltoclientmessage()
);
} else if (message.hasWebrtcstartmessage()) { } else if (message.hasWebrtcstartmessage()) {
this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage()); this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
} else if (message.hasWebrtcdisconnectmessage()) { } else if (message.hasWebrtcdisconnectmessage()) {
@ -205,10 +223,9 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasRefreshroommessage()) { } else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed. //todo: implement a way to notify the user the room was refreshed.
} else { } else {
throw new Error('Unknown message received'); throw new Error("Unknown message received");
} }
};
}
} }
private dispatch(event: string, payload: unknown): void { private dispatch(event: string, payload: unknown): void {
@ -243,16 +260,16 @@ export class RoomConnection implements RoomConnection {
positionMessage.setY(Math.floor(y)); positionMessage.setY(Math.floor(y));
let directionEnum: Direction; let directionEnum: Direction;
switch (direction) { switch (direction) {
case 'up': case "up":
directionEnum = Direction.UP; directionEnum = Direction.UP;
break; break;
case 'down': case "down":
directionEnum = Direction.DOWN; directionEnum = Direction.DOWN;
break; break;
case 'left': case "left":
directionEnum = Direction.LEFT; directionEnum = Direction.LEFT;
break; break;
case 'right': case "right":
directionEnum = Direction.RIGHT; directionEnum = Direction.RIGHT;
break; break;
default: default:
@ -327,15 +344,17 @@ export class RoomConnection implements RoomConnection {
private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined { private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
const position = message.getPosition(); const position = message.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Invalid JOIN_ROOM message'); throw new Error("Invalid JOIN_ROOM message");
} }
const characterLayers = message.getCharacterlayersList().map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => { const characterLayers = message
return { .getCharacterlayersList()
name: characterLayer.getName(), .map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
img: characterLayer.getUrl() return {
} name: characterLayer.getName(),
}) img: characterLayer.getUrl(),
};
});
const companion = message.getCompanion(); const companion = message.getCompanion();
@ -345,8 +364,9 @@ export class RoomConnection implements RoomConnection {
characterLayers, characterLayers,
visitCardUrl: message.getVisitcardurl(), visitCardUrl: message.getVisitcardurl(),
position: ProtobufClientUtils.toPointInterface(position), position: ProtobufClientUtils.toPointInterface(position),
companion: companion ? companion.getName() : null companion: companion ? companion.getName() : null,
} userUuid: message.getUseruuid(),
};
} }
public onUserMoved(callback: (message: UserMovedMessage) => void): void { public onUserMoved(callback: (message: UserMovedMessage) => void): void {
@ -372,7 +392,9 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void { public onGroupUpdatedOrCreated(
callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void
): void {
this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => { this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => {
callback(this.toGroupCreatedUpdatedMessage(message)); callback(this.toGroupCreatedUpdatedMessage(message));
}); });
@ -381,14 +403,14 @@ export class RoomConnection implements RoomConnection {
private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface { private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
const position = message.getPosition(); const position = message.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Missing position in GROUP_CREATE_UPDATE'); throw new Error("Missing position in GROUP_CREATE_UPDATE");
} }
return { return {
groupId: message.getGroupid(), groupId: message.getGroupid(),
position: position.toObject(), position: position.toObject(),
groupSize: message.getGroupsize() groupSize: message.getGroupsize(),
} };
} }
public onGroupDeleted(callback: (groupId: number) => void): void { public onGroupDeleted(callback: (groupId: number) => void): void {
@ -404,7 +426,7 @@ export class RoomConnection implements RoomConnection {
} }
public onConnectError(callback: (error: Event) => void): void { public onConnectError(callback: (error: Event) => void): void {
this.socket.addEventListener('error', callback) this.socket.addEventListener("error", callback);
} }
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void { public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
@ -445,7 +467,6 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => { this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => {
callback({ callback({
userId: message.getUserid(), userId: message.getUserid(),
name: message.getName(),
initiator: message.getInitiator(), initiator: message.getInitiator(),
webRtcUser: message.getWebrtcusername() ?? undefined, webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined, webRtcPassword: message.getWebrtcpassword() ?? undefined,
@ -476,11 +497,11 @@ export class RoomConnection implements RoomConnection {
} }
public onServerDisconnected(callback: () => void): void { public onServerDisconnected(callback: () => void): void {
this.socket.addEventListener('close', (event) => { this.socket.addEventListener("close", (event) => {
if (this.closed === true || connectionManager.unloading) { if (this.closed === true || connectionManager.unloading) {
return; return;
} }
console.log('Socket closed with code ' + event.code + ". Reason: " + event.reason); console.log("Socket closed with code " + event.code + ". Reason: " + event.reason);
if (event.code === 1000) { if (event.code === 1000) {
// Normal closure case // Normal closure case
return; return;
@ -490,14 +511,14 @@ export class RoomConnection implements RoomConnection {
} }
public getUserId(): number { public getUserId(): number {
if (this.userId === null) throw 'UserId cannot be null!' if (this.userId === null) throw "UserId cannot be null!";
return this.userId; return this.userId;
} }
disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void { disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => { this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
callback({ callback({
userId: message.getUserid() userId: message.getUserid(),
}); });
}); });
} }
@ -521,21 +542,22 @@ export class RoomConnection implements RoomConnection {
itemId: message.getItemid(), itemId: message.getItemid(),
event: message.getEvent(), event: message.getEvent(),
parameters: JSON.parse(message.getParametersjson()), parameters: JSON.parse(message.getParametersjson()),
state: JSON.parse(message.getStatejson()) state: JSON.parse(message.getStatejson()),
}); });
}); });
} }
public uploadAudio(file: FormData) { public uploadAudio(file: FormData) {
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: { data: {} }) => { return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file)
return res.data; .then((res: { data: {} }) => {
}).catch((err) => { return res.data;
console.error(err); })
throw err; .catch((err) => {
}); console.error(err);
throw err;
});
} }
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) { public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => { return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
callback({ callback({
@ -570,9 +592,9 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void { public emitReportPlayerMessage(reportedUserUuid: string, reportComment: string): void {
const reportPlayerMessage = new ReportPlayerMessage(); const reportPlayerMessage = new ReportPlayerMessage();
reportPlayerMessage.setReporteduserid(reportedUserId); reportPlayerMessage.setReporteduseruuid(reportedUserUuid);
reportPlayerMessage.setReportcomment(reportComment); reportPlayerMessage.setReportcomment(reportComment);
const clientToServerMessage = new ClientToServerMessage(); const clientToServerMessage = new ClientToServerMessage();
@ -605,12 +627,12 @@ export class RoomConnection implements RoomConnection {
} }
public isAdmin(): boolean { public isAdmin(): boolean {
return this.hasTag('admin'); return this.hasTag("admin");
} }
public emitEmoteEvent(emoteName: string): void { public emitEmoteEvent(emoteName: string): void {
const emoteMessage = new EmotePromptMessage(); const emoteMessage = new EmotePromptMessage();
emoteMessage.setEmote(emoteName) emoteMessage.setEmote(emoteName);
const clientToServerMessage = new ClientToServerMessage(); const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setEmotepromptmessage(emoteMessage); clientToServerMessage.setEmotepromptmessage(emoteMessage);
@ -618,7 +640,7 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public getAllTags() : string[] { public getAllTags(): string[] {
return this.tags; return this.tags;
} }
} }

View File

@ -1,7 +1,7 @@
import {discussionManager} from "../../WebRtc/DiscussionManager"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; import { chatVisibilityStore } from "../../Stores/ChatStore";
export const openChatIconName = 'openChatIcon'; export const openChatIconName = "openChatIcon";
export class OpenChatIcon extends Phaser.GameObjects.Image { export class OpenChatIcon extends Phaser.GameObjects.Image {
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, openChatIconName, 3); super(scene, x, y, openChatIconName, 3);
@ -9,9 +9,9 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
this.setScrollFactor(0, 0); this.setScrollFactor(0, 0);
this.setOrigin(0, 1); this.setOrigin(0, 1);
this.setInteractive(); this.setInteractive();
this.setVisible(false); //this.setVisible(false);
this.setDepth(DEPTH_INGAME_TEXT_INDEX); this.setDepth(DEPTH_INGAME_TEXT_INDEX);
this.on("pointerup", () => discussionManager.showDiscussionPart()); this.on("pointerup", () => chatVisibilityStore.set(true));
} }
} }

View File

@ -44,7 +44,6 @@ export class TextUtils {
options.align = object.text.halign; options.align = object.text.halign;
} }
console.warn(options);
const textElem = scene.add.text(object.x, object.y, object.text.text, options); const textElem = scene.add.text(object.x, object.y, object.text.text, options);
textElem.setAngle(object.rotation); textElem.setAngle(object.rotation);
} }

View File

@ -101,7 +101,6 @@ export const createLoadingPromise = (
frameConfig: FrameConfig frameConfig: FrameConfig
) => { ) => {
return new Promise<BodyResourceDescriptionInterface>((res, rej) => { return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
console.log("count", loadPlugin.listenerCount("loaderror"));
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
return res(playerResourceDescriptor); return res(playerResourceDescriptor);
} }

View File

@ -1,11 +1,6 @@
import type {PointInterface} from "../../Connexion/ConnexionModels"; import type {PointInterface} from "../../Connexion/ConnexionModels";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import type {PlayerInterface} from "./PlayerInterface";
export interface AddPlayerInterface { export interface AddPlayerInterface extends PlayerInterface {
userId: number;
name: string;
characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
visitCardUrl: string|null;
companion: string|null;
} }

View File

@ -1,17 +1,17 @@
import {ResizableScene} from "../Login/ResizableScene"; import { ResizableScene } from "../Login/ResizableScene";
import GameObject = Phaser.GameObjects.GameObject; import GameObject = Phaser.GameObjects.GameObject;
import Events = Phaser.Scenes.Events; import Events = Phaser.Scenes.Events;
import AnimationEvents = Phaser.Animations.Events; import AnimationEvents = Phaser.Animations.Events;
import StructEvents = Phaser.Structs.Events; import StructEvents = Phaser.Structs.Events;
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable"; import { SKIP_RENDER_OPTIMIZATIONS } from "../../Enum/EnvironmentVariable";
/** /**
* A scene that can track its dirty/pristine state. * A scene that can track its dirty/pristine state.
*/ */
export abstract class DirtyScene extends ResizableScene { export abstract class DirtyScene extends ResizableScene {
private isAlreadyTracking: boolean = false; private isAlreadyTracking: boolean = false;
protected dirty:boolean = true; protected dirty: boolean = true;
private objectListChanged:boolean = true; private objectListChanged: boolean = true;
private physicsEnabled: boolean = false; private physicsEnabled: boolean = false;
/** /**
@ -59,7 +59,6 @@ export abstract class DirtyScene extends ResizableScene {
this.physicsEnabled = false; this.physicsEnabled = false;
} }
}); });
} }
private trackAnimation(): void { private trackAnimation(): void {
@ -71,7 +70,7 @@ export abstract class DirtyScene extends ResizableScene {
} }
public markDirty(): void { public markDirty(): void {
this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => this.dirty = true); this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => (this.dirty = true));
} }
public onResize(): void { public onResize(): void {

View File

@ -1,26 +1,24 @@
import {GameScene} from "./GameScene"; import { GameScene } from "./GameScene";
import {connectionManager} from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import type {Room} from "../../Connexion/Room"; import type { Room } from "../../Connexion/Room";
import {MenuScene, MenuSceneName} from "../Menu/MenuScene"; import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import {LoginSceneName} from "../Login/LoginScene"; import { LoginSceneName } from "../Login/LoginScene";
import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import { SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import {EnableCameraSceneName} from "../Login/EnableCameraScene"; import { EnableCameraSceneName } from "../Login/EnableCameraScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import {get} from "svelte/store"; import { get } from "svelte/store";
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore"; import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore";
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore"; import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore";
/** /**
* This class should be responsible for any scene starting/stopping * This class should be responsible for any scene starting/stopping
*/ */
export class GameManager { export class GameManager {
private playerName: string|null; private playerName: string | null;
private characterLayers: string[]|null; private characterLayers: string[] | null;
private companion: string|null; private companion: string | null;
private startRoom!:Room; private startRoom!: Room;
currentGameSceneName: string|null = null; currentGameSceneName: string | null = null;
constructor() { constructor() {
this.playerName = localUserStore.getName(); this.playerName = localUserStore.getName();
@ -30,7 +28,7 @@ export class GameManager {
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> { public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
this.startRoom = await connectionManager.initGameConnexion(); this.startRoom = await connectionManager.initGameConnexion();
await this.loadMap(this.startRoom, scenePlugin); this.loadMap(this.startRoom, scenePlugin);
if (!this.playerName) { if (!this.playerName) {
return LoginSceneName; return LoginSceneName;
@ -51,43 +49,44 @@ export class GameManager {
localUserStore.setCharacterLayers(layers); localUserStore.setCharacterLayers(layers);
} }
getPlayerName(): string|null { getPlayerName(): string | null {
return this.playerName; return this.playerName;
} }
getCharacterLayers(): string[] { getCharacterLayers(): string[] {
if (!this.characterLayers) { if (!this.characterLayers) {
throw 'characterLayers are not set'; throw "characterLayers are not set";
} }
return this.characterLayers; return this.characterLayers;
} }
setCompanion(companion: string | null): void {
setCompanion(companion: string|null): void {
this.companion = companion; this.companion = companion;
} }
getCompanion(): string|null { getCompanion(): string | null {
return this.companion; return this.companion;
} }
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> { public loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin) {
const roomID = room.id; const roomID = room.key;
const mapDetail = await room.getMapDetail();
const gameIndex = scenePlugin.getIndex(roomID); const gameIndex = scenePlugin.getIndex(roomID);
if(gameIndex === -1){ if (gameIndex === -1) {
const game : Phaser.Scene = new GameScene(room, mapDetail.mapUrl); const game: Phaser.Scene = new GameScene(room, room.mapUrl);
scenePlugin.add(roomID, game, false); scenePlugin.add(roomID, game, false);
} }
} }
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void { public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id)) console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
scenePlugin.start(this.currentGameSceneName || this.startRoom.id); scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
scenePlugin.launch(MenuSceneName); scenePlugin.launch(MenuSceneName);
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){ if (
!localUserStore.getHelpCameraSettingsShown() &&
(!get(requestedMicrophoneState) || !get(requestedCameraState))
) {
helpCameraSettingsVisibleStore.set(true); helpCameraSettingsVisibleStore.set(true);
localUserStore.setHelpCameraSettingsShown(); localUserStore.setHelpCameraSettingsShown();
} }
@ -104,7 +103,7 @@ export class GameManager {
* This will close the socket connections and stop the gameScene, but won't remove it. * This will close the socket connections and stop the gameScene, but won't remove it.
*/ */
leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void { leaveGame(scene: Phaser.Scene, targetSceneName: string, sceneClass: Phaser.Scene): void {
if (this.currentGameSceneName === null) throw 'No current scene id set!'; if (this.currentGameSceneName === null) throw "No current scene id set!";
const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene; const gameScene: GameScene = scene.scene.get(this.currentGameSceneName) as GameScene;
gameScene.cleanupClosingScene(); gameScene.cleanupClosingScene();
scene.scene.stop(this.currentGameSceneName); scene.scene.stop(this.currentGameSceneName);
@ -123,13 +122,13 @@ export class GameManager {
scene.scene.start(this.currentGameSceneName); scene.scene.start(this.currentGameSceneName);
scene.scene.wake(MenuSceneName); scene.scene.wake(MenuSceneName);
} else { } else {
scene.scene.run(fallbackSceneName) scene.scene.run(fallbackSceneName);
} }
} }
public getCurrentGameScene(scene: Phaser.Scene): GameScene { public getCurrentGameScene(scene: Phaser.Scene): GameScene {
if (this.currentGameSceneName === null) throw 'No current scene id set!'; if (this.currentGameSceneName === null) throw "No current scene id set!";
return scene.scene.get(this.currentGameSceneName) as GameScene return scene.scene.get(this.currentGameSceneName) as GameScene;
} }
} }

View File

@ -1,9 +1,13 @@
import type {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty} from "../Map/ITiledMap"; import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void; export type PropertyChangeCallback = (
newValue: string | number | boolean | undefined,
oldValue: string | number | boolean | undefined,
allProps: Map<string, string | boolean | number>
) => void;
/** /**
* A wrapper around a ITiledMap interface to provide additional capabilities. * A wrapper around a ITiledMap interface to provide additional capabilities.
@ -19,37 +23,50 @@ export class GameMap {
public readonly flatLayers: ITiledMapLayer[]; public readonly flatLayers: ITiledMapLayer[];
public readonly phaserLayers: TilemapLayer[] = []; public readonly phaserLayers: TilemapLayer[] = [];
public exitUrls: Array<string> = [] public exitUrls: Array<string> = [];
public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array<Phaser.Tilemaps.Tileset>) { public hasStartTile = false;
public constructor(
private map: ITiledMap,
phaserMap: Phaser.Tilemaps.Tilemap,
terrains: Array<Phaser.Tilemaps.Tileset>
) {
this.flatLayers = flattenGroupLayersMap(map); this.flatLayers = flattenGroupLayersMap(map);
let depth = -2; let depth = -2;
for (const layer of this.flatLayers) { for (const layer of this.flatLayers) {
if(layer.type === 'tilelayer'){ if (layer.type === "tilelayer") {
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth)); this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
} }
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { if (layer.type === "objectgroup" && layer.name === "floorLayer") {
depth = DEPTH_OVERLAY_INDEX; depth = DEPTH_OVERLAY_INDEX;
} }
} }
for (const tileset of map.tilesets) { for (const tileset of map.tilesets) {
tileset?.tiles?.forEach(tile => { tileset?.tiles?.forEach((tile) => {
if (tile.properties) { if (tile.properties) {
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties;
tile.properties.forEach(prop => { tile.properties.forEach((prop) => {
if (prop.name == 'name' && typeof prop.value == "string") { if (prop.name == "name" && typeof prop.value == "string") {
this.tileNameMap.set(prop.value, tileset.firstgid + tile.id); this.tileNameMap.set(prop.value, tileset.firstgid + tile.id);
} }
if (prop.name == "exitUrl" && typeof prop.value == "string") { if (prop.name == "exitUrl" && typeof prop.value == "string") {
this.exitUrls.push(prop.value); this.exitUrls.push(prop.value);
} else if (prop.name == "start") {
this.hasStartTile = true;
} }
}) });
} }
}) });
} }
} }
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}
return [];
}
/** /**
* Sets the position of the current player (in pixels) * Sets the position of the current player (in pixels)
@ -93,7 +110,7 @@ export class GameMap {
const properties = new Map<string, string | boolean | number>(); const properties = new Map<string, string | boolean | number>();
for (const layer of this.flatLayers) { for (const layer of this.flatLayers) {
if (layer.type !== 'tilelayer') { if (layer.type !== "tilelayer") {
continue; continue;
} }
@ -103,7 +120,7 @@ export class GameMap {
if (tiles[key] == 0) { if (tiles[key] == 0) {
continue; continue;
} }
tileIndex = tiles[key] tileIndex = tiles[key];
} }
// There is a tile in this layer, let's embed the properties // There is a tile in this layer, let's embed the properties
@ -117,28 +134,36 @@ export class GameMap {
} }
if (tileIndex) { if (tileIndex) {
this.tileSetPropertyMap[tileIndex]?.forEach(property => { this.tileSetPropertyMap[tileIndex]?.forEach((property) => {
if (property.value) { if (property.value) {
properties.set(property.name, property.value) properties.set(property.name, property.value);
} else if (properties.has(property.name)) { } else if (properties.has(property.name)) {
properties.delete(property.name) properties.delete(property.name);
} }
}) });
} }
} }
return properties; return properties;
} }
public getMap(): ITiledMap{ public getMap(): ITiledMap {
return this.map; return this.map;
} }
private getTileProperty(index: number): Array<ITiledMapLayerProperty> { private getTileProperty(index: number): Array<ITiledMapLayerProperty> {
return this.tileSetPropertyMap[index]; if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}
return [];
} }
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) { private trigger(
propName: string,
oldValue: string | number | boolean | undefined,
newValue: string | number | boolean | undefined,
allProps: Map<string, string | boolean | number>
) {
const callbacksArray = this.callbacks.get(propName); const callbacksArray = this.callbacks.get(propName);
if (callbacksArray !== undefined) { if (callbacksArray !== undefined) {
for (const callback of callbacksArray) { for (const callback of callbacksArray) {
@ -167,7 +192,11 @@ export class GameMap {
return this.phaserLayers.find((layer) => layer.layer.name === layerName); return this.phaserLayers.find((layer) => layer.layer.name === layerName);
} }
public addTerrain(terrain : Phaser.Tilemaps.Tileset): void { public findPhaserLayers(groupName: string): TilemapLayer[] {
return this.phaserLayers.filter((l) => l.layer.name.includes(groupName));
}
public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
for (const phaserLayer of this.phaserLayers) { for (const phaserLayer of this.phaserLayers) {
phaserLayer.tileset.push(terrain); phaserLayer.tileset.push(terrain);
} }
@ -175,40 +204,46 @@ export class GameMap {
private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void { private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void {
const fLayer = this.findLayer(layer); const fLayer = this.findLayer(layer);
if ( fLayer == undefined ) { if (fLayer == undefined) {
console.error("The layer that you want to change doesn't exist."); console.error("The layer '" + layer + "' that you want to change doesn't exist.");
return; return;
} }
if (fLayer.type !== 'tilelayer') { if (fLayer.type !== "tilelayer") {
console.error("The layer that you want to change is not a tilelayer. Tile can only be put in tilelayer."); console.error(
"The layer '" +
layer +
"' that you want to change is not a tilelayer. Tile can only be put in tilelayer."
);
return; return;
} }
if (typeof fLayer.data === "string") { if (typeof fLayer.data === "string") {
console.error("Data of the layer that you want to change is only readable."); console.error("Data of the layer '" + layer + "' that you want to change is only readable.");
return; return;
} }
fLayer.data[x+y*fLayer.height] = index; fLayer.data[x + y * fLayer.width] = index;
} }
public putTile(tile: string | number, x: number, y: number, layer: string): void { public putTile(tile: string | number | null, x: number, y: number, layer: string): void {
const phaserLayer = this.findPhaserLayer(layer); const phaserLayer = this.findPhaserLayer(layer);
if ( phaserLayer ) { if (phaserLayer) {
if (tile === null) {
phaserLayer.putTileAt(-1, x, y);
return;
}
const tileIndex = this.getIndexForTileType(tile); const tileIndex = this.getIndexForTileType(tile);
if ( tileIndex !== undefined ) { if (tileIndex !== undefined) {
this.putTileInFlatLayer(tileIndex, x, y, layer); this.putTileInFlatLayer(tileIndex, x, y, layer);
const phaserTile = phaserLayer.putTileAt(tileIndex, x, y); const phaserTile = phaserLayer.putTileAt(tileIndex, x, y);
for (const property of this.getTileProperty(tileIndex)) { for (const property of this.getTileProperty(tileIndex)) {
if ( property.name === "collides" && property.value === "true") { if (property.name === "collides" && property.value) {
phaserTile.setCollision(true); phaserTile.setCollision(true);
} }
} }
} else {
console.error("The tile '" + tile + "' that you want to place doesn't exist.");
} }
else { } else {
console.error("The tile that you want to place doesn't exist."); console.error("The layer '" + layer + "' does not exist (or is not a tilelaye).");
}
}
else {
console.error("The layer that you want to change is not a tilelayer. Tile can only be put in tilelayer.");
} }
} }
@ -218,5 +253,4 @@ export class GameMap {
} }
return this.tileNameMap.get(tile); return this.tileNameMap.get(tile);
} }
} }

View File

@ -1,4 +1,3 @@
import { Queue } from "queue-typescript";
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
import { userMessageManager } from "../../Administration/UserMessageManager"; import { userMessageManager } from "../../Administration/UserMessageManager";
@ -14,20 +13,9 @@ import type {
PositionInterface, PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
} from "../../Connexion/ConnexionModels"; } from "../../Connexion/ConnexionModels";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { Room } from "../../Connexion/Room";
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { TextureError } from "../../Exception/TextureError";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; import { Queue } from "queue-typescript";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { urlManager } from "../../Url/UrlManager";
import { audioManager } from "../../WebRtc/AudioManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { import {
AUDIO_LOOP_PROPERTY, AUDIO_LOOP_PROPERTY,
AUDIO_VOLUME_PROPERTY, AUDIO_VOLUME_PROPERTY,
@ -39,15 +27,21 @@ import {
TRIGGER_WEBSITE_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES,
WEBSITE_MESSAGE_PROPERTIES, WEBSITE_MESSAGE_PROPERTIES,
} from "../../WebRtc/LayoutManager"; } from "../../WebRtc/LayoutManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { urlManager } from "../../Url/UrlManager";
import { audioManager } from "../../WebRtc/AudioManager";
import { TextureError } from "../../Exception/TextureError";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { mediaManager } from "../../WebRtc/MediaManager"; import { mediaManager } from "../../WebRtc/MediaManager";
import { SimplePeer, UserSimplePeerInterface } from "../../WebRtc/SimplePeer"; import { SimplePeer } from "../../WebRtc/SimplePeer";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { ChatModeIcon } from "../Components/ChatModeIcon";
import { addLoader } from "../Components/Loader"; import { addLoader } from "../Components/Loader";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon";
import { PresentationModeIcon } from "../Components/PresentationModeIcon";
import { TextUtils } from "../Components/TextUtils";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { RemotePlayer } from "../Entity/RemotePlayer"; import { RemotePlayer } from "../Entity/RemotePlayer";
import type { ActionableItem } from "../Items/ActionableItem"; import type { ActionableItem } from "../Items/ActionableItem";
@ -58,7 +52,6 @@ import type {
ITiledMapLayer, ITiledMapLayer,
ITiledMapLayerProperty, ITiledMapLayerProperty,
ITiledMapObject, ITiledMapObject,
ITiledMapTileLayer,
ITiledTileSet, ITiledTileSet,
} from "../Map/ITiledMap"; } from "../Map/ITiledMap";
import { MenuScene, MenuSceneName } from "../Menu/MenuScene"; import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
@ -66,13 +59,8 @@ import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
import { ErrorSceneName } from "../Reconnecting/ErrorScene"; import { ErrorSceneName } from "../Reconnecting/ErrorScene";
import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
import { waScaleManager } from "../Services/WaScaleManager";
import { PinchManager } from "../UserInput/PinchManager";
import { UserInputManager } from "../UserInput/UserInputManager"; import { UserInputManager } from "../UserInput/UserInputManager";
import type { AddPlayerInterface } from "./AddPlayerInterface"; import type { AddPlayerInterface } from "./AddPlayerInterface";
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
import { DirtyScene } from "./DirtyScene";
import { EmoteManager } from "./EmoteManager";
import { gameManager } from "./GameManager"; import { gameManager } from "./GameManager";
import { GameMap } from "./GameMap"; import { GameMap } from "./GameMap";
import { PlayerMovement } from "./PlayerMovement"; import { PlayerMovement } from "./PlayerMovement";
@ -83,16 +71,28 @@ import CanvasTexture = Phaser.Textures.CanvasTexture;
import GameObject = Phaser.GameObjects.GameObject; import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import DOMElement = Phaser.GameObjects.DOMElement; import DOMElement = Phaser.GameObjects.DOMElement;
import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { DirtyScene } from "./DirtyScene";
import { TextUtils } from "../Components/TextUtils";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager";
import EVENT_TYPE = Phaser.Scenes.Events; import EVENT_TYPE = Phaser.Scenes.Events;
import RenderTexture = Phaser.GameObjects.RenderTexture; import RenderTexture = Phaser.GameObjects.RenderTexture;
import Tilemap = Phaser.Tilemaps.Tilemap; import Tilemap = Phaser.Tilemaps.Tilemap;
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import AnimatedTiles from "phaser-animated-tiles"; import AnimatedTiles from "phaser-animated-tiles";
import { StartPositionCalculator } from "./StartPositionCalculator";
import { soundManager } from "./SoundManager"; import { soundManager } from "./SoundManager";
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -129,8 +129,6 @@ interface DeleteGroupEventInterface {
groupId: number; groupId: number;
} }
const defaultStartLayerName = "start";
export class GameScene extends DirtyScene { export class GameScene extends DirtyScene {
Terrains: Array<Phaser.Tilemaps.Tileset>; Terrains: Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: Player; CurrentPlayer!: Player;
@ -141,8 +139,6 @@ export class GameScene extends DirtyScene {
mapFile!: ITiledMap; mapFile!: ITiledMap;
animatedTiles!: AnimatedTiles; animatedTiles!: AnimatedTiles;
groups: Map<number, Sprite>; groups: Map<number, Sprite>;
startX!: number;
startY!: number;
circleTexture!: CanvasTexture; circleTexture!: CanvasTexture;
circleRedTexture!: CanvasTexture; circleRedTexture!: CanvasTexture;
pendingEvents: Queue< pendingEvents: Queue<
@ -174,9 +170,10 @@ export class GameScene extends DirtyScene {
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void; private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
private iframeSubscriptionList!: Array<Subscription>; private iframeSubscriptionList!: Array<Subscription>;
private peerStoreUnsubscribe!: () => void; private peerStoreUnsubscribe!: () => void;
private chatVisibilityUnsubscribe!: () => void;
private biggestAvailableAreaStoreUnsubscribe!: () => void; private biggestAvailableAreaStoreUnsubscribe!: () => void;
MapUrlFile: string; MapUrlFile: string;
RoomId: string; roomUrl: string;
instance: string; instance: string;
currentTick!: number; currentTick!: number;
@ -194,7 +191,6 @@ export class GameScene extends DirtyScene {
private outlinedItem: ActionableItem | null = null; private outlinedItem: ActionableItem | null = null;
public userInputManager!: UserInputManager; public userInputManager!: UserInputManager;
private isReconnecting: boolean | undefined = undefined; private isReconnecting: boolean | undefined = undefined;
private startLayerName!: string | null;
private openChatIcon!: OpenChatIcon; private openChatIcon!: OpenChatIcon;
private playerName!: string; private playerName!: string;
private characterLayers!: string[]; private characterLayers!: string[];
@ -206,17 +202,18 @@ export class GameScene extends DirtyScene {
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private emoteManager!: EmoteManager; private emoteManager!: EmoteManager;
private preloading: boolean = true; private preloading: boolean = true;
startPositionCalculator!: StartPositionCalculator;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({ super({
key: customKey ?? room.id, key: customKey ?? room.key,
}); });
this.Terrains = []; this.Terrains = [];
this.groups = new Map<number, Sprite>(); this.groups = new Map<number, Sprite>();
this.instance = room.getInstance(); this.instance = room.getInstance();
this.MapUrlFile = MapUrlFile; this.MapUrlFile = MapUrlFile;
this.RoomId = room.id; this.roomUrl = room.key;
this.createPromise = new Promise<void>((resolve, reject): void => { this.createPromise = new Promise<void>((resolve, reject): void => {
this.createPromiseResolve = resolve; this.createPromiseResolve = resolve;
@ -426,7 +423,6 @@ export class GameScene extends DirtyScene {
gameManager.gameSceneIsCreated(this); gameManager.gameSceneIsCreated(this);
urlManager.pushRoomIdToUrl(this.room); urlManager.pushRoomIdToUrl(this.room);
this.startLayerName = urlManager.getStartLayerNameFromUrl();
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
this.pinchManager = new PinchManager(this); this.pinchManager = new PinchManager(this);
@ -469,11 +465,13 @@ export class GameScene extends DirtyScene {
if (layer.type === "tilelayer") { if (layer.type === "tilelayer") {
const exitSceneUrl = this.getExitSceneUrl(layer); const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl !== undefined) { if (exitSceneUrl !== undefined) {
this.loadNextGame(exitSceneUrl); this.loadNextGame(
Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile)
);
} }
const exitUrl = this.getExitUrl(layer); const exitUrl = this.getExitUrl(layer);
if (exitUrl !== undefined) { if (exitUrl !== undefined) {
this.loadNextGame(exitUrl); this.loadNextGameFromExitUrl(exitUrl);
} }
} }
if (layer.type === "objectgroup") { if (layer.type === "objectgroup") {
@ -486,10 +484,15 @@ export class GameScene extends DirtyScene {
} }
this.gameMap.exitUrls.forEach((exitUrl) => { this.gameMap.exitUrls.forEach((exitUrl) => {
this.loadNextGame(exitUrl); this.loadNextGameFromExitUrl(exitUrl);
}); });
this.initStartXAndStartY(); this.startPositionCalculator = new StartPositionCalculator(
this.gameMap,
this.mapFile,
this.initPosition,
urlManager.getStartLayerNameFromUrl()
);
//add entities //add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>(); this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
@ -572,6 +575,10 @@ export class GameScene extends DirtyScene {
} }
oldPeerNumber = newPeerNumber; oldPeerNumber = newPeerNumber;
}); });
this.chatVisibilityUnsubscribe = chatVisibilityStore.subscribe((v) => {
this.openChatIcon.setVisible(!v);
});
} }
/** /**
@ -582,12 +589,11 @@ export class GameScene extends DirtyScene {
connectionManager connectionManager
.connectToRoomSocket( .connectToRoomSocket(
this.RoomId, this.roomUrl,
this.playerName, this.playerName,
this.characterLayers, this.characterLayers,
{ {
x: this.startX, ...this.startPositionCalculator.startPosition,
y: this.startY,
}, },
{ {
left: camera.scrollX, left: camera.scrollX,
@ -600,6 +606,8 @@ export class GameScene extends DirtyScene {
.then((onConnect: OnConnectInterface) => { .then((onConnect: OnConnectInterface) => {
this.connection = onConnect.connection; this.connection = onConnect.connection;
playersStore.connectToRoomConnection(this.connection);
this.connection.onUserJoins((message: MessageUserJoined) => { this.connection.onUserJoins((message: MessageUserJoined) => {
const userMessage: AddPlayerInterface = { const userMessage: AddPlayerInterface = {
userId: message.userId, userId: message.userId,
@ -608,6 +616,7 @@ export class GameScene extends DirtyScene {
position: message.position, position: message.position,
visitCardUrl: message.visitCardUrl, visitCardUrl: message.visitCardUrl,
companion: message.companion, companion: message.companion,
userUuid: message.userUuid,
}; };
this.addPlayer(userMessage); this.addPlayer(userMessage);
}); });
@ -691,12 +700,12 @@ export class GameScene extends DirtyScene {
const self = this; const self = this;
this.simplePeer.registerPeerConnectionListener({ this.simplePeer.registerPeerConnectionListener({
onConnect(peer) { onConnect(peer) {
self.openChatIcon.setVisible(true); //self.openChatIcon.setVisible(true);
audioManager.decreaseVolume(); audioManager.decreaseVolume();
}, },
onDisconnect(userId: number) { onDisconnect(userId: number) {
if (self.simplePeer.getNbConnections() === 0) { if (self.simplePeer.getNbConnections() === 0) {
self.openChatIcon.setVisible(false); //self.openChatIcon.setVisible(false);
audioManager.restoreVolume(); audioManager.restoreVolume();
} }
}, },
@ -768,10 +777,13 @@ export class GameScene extends DirtyScene {
private triggerOnMapLayerPropertyChange() { private triggerOnMapLayerPropertyChange() {
this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => { this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => {
if (newValue) this.onMapExit(newValue as string); if (newValue)
this.onMapExit(
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
);
}); });
this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => { this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => {
if (newValue) this.onMapExit(newValue as string); if (newValue) this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()));
}); });
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => { this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
if (newValue === undefined) { if (newValue === undefined) {
@ -989,16 +1001,16 @@ ${escapedMessage}
}) })
); );
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.enablePlayerControlStream.subscribe(()=>{ iframeListener.enablePlayerControlStream.subscribe(() => {
this.userInputManager.restoreControls(); this.userInputManager.restoreControls();
}) })
); );
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.loadPageStream.subscribe((url: string) => { iframeListener.loadPageStream.subscribe((url: string) => {
this.loadNextGame(url).then(() => { this.loadNextGameFromExitUrl(url).then(() => {
this.events.once(EVENT_TYPE.POST_UPDATE, () => { this.events.once(EVENT_TYPE.POST_UPDATE, () => {
this.onMapExit(url); this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString()));
}); });
}); });
}) })
@ -1047,24 +1059,23 @@ ${escapedMessage}
}) })
); );
iframeListener.registerAnswerer("getState", () => {
return {
mapUrl: this.MapUrlFile,
startLayerName: this.startPositionCalculator.startLayerName,
uuid: localUserStore.getLocalUser()?.uuid,
nickname: localUserStore.getName(),
roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [],
};
});
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.gameStateStream.subscribe(() => { iframeListener.setTilesStream.subscribe((eventTiles) => {
iframeListener.sendGameStateEvent({ for (const eventTile of eventTiles) {
mapUrl: this.MapUrlFile, this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
startLayerName: this.startLayerName, }
uuid: localUserStore.getLocalUser()?.uuid,
nickname: localUserStore.getName(),
roomId: this.RoomId,
tags: this.connection ? this.connection.getAllTags() : [],
});
}) })
) );
this.iframeSubscriptionList.push(iframeListener.setTilesStream.subscribe((eventTiles) => {
for (const eventTile of eventTiles) {
this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
}
}))
} }
private setPropertyLayer( private setPropertyLayer(
@ -1077,53 +1088,88 @@ ${escapedMessage}
console.warn('Could not find layer "' + layerName + '" when calling setProperty'); console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return; return;
} }
const property = (layer.properties as ITiledMapLayerProperty[])?.find( if (propertyName === "exitUrl" && typeof propertyValue === "string") {
(property) => property.name === propertyName this.loadNextGameFromExitUrl(propertyValue);
); }
if (property === undefined) { if (layer.properties === undefined) {
layer.properties = []; layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
if (propertyValue === undefined) {
return;
}
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue }); layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return; return;
} }
if (propertyValue === undefined) {
const index = layer.properties.indexOf(property);
layer.properties.splice(index, 1);
}
property.value = propertyValue; property.value = propertyValue;
} }
private setLayerVisibility(layerName: string, visible: boolean): void { private setLayerVisibility(layerName: string, visible: boolean): void {
const phaserLayer = this.gameMap.findPhaserLayer(layerName); const phaserLayer = this.gameMap.findPhaserLayer(layerName);
if (phaserLayer === undefined) { if (phaserLayer != undefined) {
console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); phaserLayer.setVisible(visible);
return; phaserLayer.setCollisionByProperty({ collides: true }, visible);
} else {
const phaserLayers = this.gameMap.findPhaserLayers(layerName + "/");
if (phaserLayers === []) {
console.warn(
'Could not find layer with name that contains "' +
layerName +
'" when calling WA.hideLayer / WA.showLayer'
);
return;
}
for (let i = 0; i < phaserLayers.length; i++) {
phaserLayers[i].setVisible(visible);
phaserLayers[i].setCollisionByProperty({ collides: true }, visible);
}
} }
phaserLayer.setVisible(visible); this.markDirty();
this.dirty = true;
} }
private getMapDirUrl(): string { private getMapDirUrl(): string {
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
} }
private onMapExit(exitKey: string) { private async onMapExit(roomUrl: URL) {
if (this.mapTransitioning) return; if (this.mapTransitioning) return;
this.mapTransitioning = true; this.mapTransitioning = true;
const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance);
if (!roomId) throw new Error("Could not find the room from its exit key: " + exitKey); let targetRoom: Room;
urlManager.pushStartLayerNameToUrl(hash); try {
targetRoom = await Room.createRoom(roomUrl);
} catch (e: unknown) {
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
this.mapTransitioning = false;
return;
}
if (roomUrl.hash) {
urlManager.pushStartLayerNameToUrl(roomUrl.hash);
}
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene; const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
menuScene.reset(); menuScene.reset();
if (roomId !== this.scene.key) {
if (this.scene.get(roomId) === null) { if (!targetRoom.isEqual(this.room)) {
console.error("next room not loaded", exitKey); if (this.scene.get(targetRoom.key) === null) {
console.error("next room not loaded", targetRoom.key);
return; return;
} }
this.cleanupClosingScene(); this.cleanupClosingScene();
this.scene.stop(); this.scene.stop();
this.scene.start(targetRoom.key);
this.scene.remove(this.scene.key); this.scene.remove(this.scene.key);
this.scene.start(roomId);
} else { } else {
//if the exit points to the current map, we simply teleport the user back to the startLayer //if the exit points to the current map, we simply teleport the user back to the startLayer
this.initPositionFromLayerName(hash || defaultStartLayerName); this.startPositionCalculator.initPositionFromLayerName(roomUrl.hash, roomUrl.hash);
this.CurrentPlayer.x = this.startX; this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
this.CurrentPlayer.y = this.startY; this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
setTimeout(() => (this.mapTransitioning = false), 500); setTimeout(() => (this.mapTransitioning = false), 500);
} }
} }
@ -1148,7 +1194,9 @@ ${escapedMessage}
this.pinchManager?.destroy(); this.pinchManager?.destroy();
this.emoteManager.destroy(); this.emoteManager.destroy();
this.peerStoreUnsubscribe(); this.peerStoreUnsubscribe();
this.chatVisibilityUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer("getState");
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();
@ -1170,46 +1218,6 @@ ${escapedMessage}
this.MapPlayersByKey = new Map<number, RemotePlayer>(); this.MapPlayersByKey = new Map<number, RemotePlayer>();
} }
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startX = this.initPosition.x;
this.startY = this.initPosition.y;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName);
}
if (this.startX === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startX === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startX = this.mapFile.width * 16;
this.startY = this.mapFile.height * 16;
}
}
private initPositionFromLayerName(layerName: string) {
for (const layer of this.gameMap.flatLayers) {
if (
(layerName === layer.name || layer.name.endsWith("/" + layerName)) &&
layer.type === "tilelayer" &&
(layerName === defaultStartLayerName || this.isStartLayer(layer))
) {
const startPosition = this.startUser(layer);
this.startX = startPosition.x + this.mapFile.tilewidth / 2;
this.startY = startPosition.y + this.mapFile.tileheight / 2;
}
}
}
private getExitUrl(layer: ITiledMapLayer): string | undefined { private getExitUrl(layer: ITiledMapLayer): string | undefined {
return this.getProperty(layer, "exitUrl") as string | undefined; return this.getProperty(layer, "exitUrl") as string | undefined;
} }
@ -1221,10 +1229,6 @@ ${escapedMessage}
return this.getProperty(layer, "exitSceneUrl") as string | undefined; return this.getProperty(layer, "exitSceneUrl") as string | undefined;
} }
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, "startLayer") == true;
}
private getScriptUrls(map: ITiledMap): string[] { private getScriptUrls(map: ITiledMap): string[] {
return (this.getProperties(map, "script") as string[]).map((script) => return (this.getProperties(map, "script") as string[]).map((script) =>
new URL(script, this.MapUrlFile).toString() new URL(script, this.MapUrlFile).toString()
@ -1255,38 +1259,18 @@ ${escapedMessage}
.map((property) => property.value); .map((property) => property.value);
} }
//todo: push that into the gameManager private loadNextGameFromExitUrl(exitUrl: string): Promise<void> {
private loadNextGame(exitSceneIdentifier: string): Promise<void> { return this.loadNextGame(Room.getRoomPathFromExitUrl(exitUrl, window.location.toString()));
const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
const room = new Room(roomId);
return gameManager.loadMap(room, this.scene).catch(() => {});
} }
private startUser(layer: ITiledMapTileLayer): PositionInterface { //todo: push that into the gameManager
const tiles = layer.data; private async loadNextGame(exitRoomPath: URL): Promise<void> {
if (typeof tiles === "string") { try {
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string"); const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene);
} catch (e: unknown) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
} }
const possibleStartPositions: PositionInterface[] = [];
tiles.forEach((objectKey: number, key: number) => {
if (objectKey === 0) {
return;
}
const y = Math.floor(key / layer.width);
const x = key % layer.width;
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
});
// Get a value at random amongst allowed values
if (possibleStartPositions.length === 0) {
console.warn('The start layer "' + layer.name + '" for this map is empty.');
return {
x: 0,
y: 0,
};
}
// Choose one of the available start positions at random amongst the list of available start positions.
return possibleStartPositions[Math.floor(Math.random() * possibleStartPositions.length)];
} }
//todo: in a dedicated class/function? //todo: in a dedicated class/function?
@ -1321,8 +1305,8 @@ ${escapedMessage}
try { try {
this.CurrentPlayer = new Player( this.CurrentPlayer = new Player(
this, this,
this.startX, this.startPositionCalculator.startPosition.x,
this.startY, this.startPositionCalculator.startPosition.y,
this.playerName, this.playerName,
texturesPromise, texturesPromise,
PlayerAnimationDirections.Down, PlayerAnimationDirections.Down,
@ -1729,7 +1713,7 @@ ${escapedMessage}
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: "Banned", title: "Banned",
subTitle: "You were banned from WorkAdventure", subTitle: "You were banned from WorkAdventure",
message: "If you want more information, you may contact us at: workadventure@thecodingmachine.com", message: "If you want more information, you may contact us at: hello@workadventu.re",
}); });
} }
@ -1744,14 +1728,14 @@ ${escapedMessage}
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: "Connection rejected", title: "Connection rejected",
subTitle: "The world you are trying to join is full. Try again later.", subTitle: "The world you are trying to join is full. Try again later.",
message: "If you want more information, you may contact us at: workadventure@thecodingmachine.com", message: "If you want more information, you may contact us at: hello@workadventu.re",
}); });
} else { } else {
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: "Connection rejected", title: "Connection rejected",
subTitle: "You cannot join the World. Try again later. \n\r \n\r Error: " + message + ".", subTitle: "You cannot join the World. Try again later. \n\r \n\r Error: " + message + ".",
message: message:
"If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com", "If you want more information, you may contact administrator or contact us at: hello@workadventu.re",
}); });
} }
} }

View File

@ -0,0 +1,11 @@
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
export interface PlayerInterface {
userId: number;
name: string;
characterLayers: BodyResourceDescriptionInterface[];
visitCardUrl: string | null;
companion: string | null;
userUuid: string;
color?: string;
}

View File

@ -1,10 +1,14 @@
import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable"; import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
import type { PositionInterface } from "../../Connexion/ConnexionModels"; import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
export class PlayerMovement { export class PlayerMovement {
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasPlayerMovedEvent, private endTick: number) { public constructor(
} private startPosition: PositionInterface,
private startTick: number,
private endPosition: HasPlayerMovedEvent,
private endTick: number
) {}
public isOutdated(tick: number): boolean { public isOutdated(tick: number): boolean {
//console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME) //console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME)
@ -24,14 +28,18 @@ export class PlayerMovement {
return this.endPosition; return this.endPosition;
} }
const x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x; const x =
const y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y; (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.x;
const y =
(this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.y;
//console.log('Computed position ', x, y) //console.log('Computed position ', x, y)
return { return {
x, x,
y, y,
direction: this.endPosition.direction, direction: this.endPosition.direction,
moving: true moving: true,
} };
} }
} }

View File

@ -2,7 +2,7 @@
* This class is in charge of computing the position of all players. * This class is in charge of computing the position of all players.
* Player movement is delayed by 200ms so position depends on ticks. * Player movement is delayed by 200ms so position depends on ticks.
*/ */
import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import type { PlayerMovement } from "./PlayerMovement"; import type { PlayerMovement } from "./PlayerMovement";
export class PlayersPositionInterpolator { export class PlayersPositionInterpolator {
@ -24,7 +24,7 @@ export class PlayersPositionInterpolator {
this.playerMovements.delete(userId); this.playerMovements.delete(userId);
} }
//console.log("moving") //console.log("moving")
positions.set(userId, playerMovement.getPosition(tick)) positions.set(userId, playerMovement.getPosition(tick));
}); });
return positions; return positions;
} }

View File

@ -0,0 +1,127 @@
import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
import type { GameMap } from "./GameMap";
const defaultStartLayerName = "start";
export class StartPositionCalculator {
public startPosition!: PositionInterface;
constructor(
private readonly gameMap: GameMap,
private readonly mapFile: ITiledMap,
private readonly initPosition: PositionInterface | null,
public readonly startLayerName: string | null
) {
this.initStartXAndStartY();
}
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
/**
*
* @param selectedLayer this is always the layer that is selected with the hash in the url
* @param selectedOrDefaultLayer this can also be the {defaultStartLayerName} if the {selectedLayer} didnt yield any start points
*/
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
if (!selectedOrDefaultLayer) {
selectedOrDefaultLayer = defaultStartLayerName;
}
for (const layer of this.gameMap.flatLayers) {
if (
(selectedOrDefaultLayer === layer.name || layer.name.endsWith("/" + selectedOrDefaultLayer)) &&
layer.type === "tilelayer" &&
(selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))
) {
const startPosition = this.startUser(layer, selectedLayer);
this.startPosition = {
x: startPosition.x + this.mapFile.tilewidth / 2,
y: startPosition.y + this.mapFile.tileheight / 2,
};
}
}
}
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, "startLayer") == true;
}
/**
*
* @param selectedLayer this is always the layer that is selected with the hash in the url
* @param selectedOrDefaultLayer this can also be the default layer if the {selectedLayer} didnt yield any start points
*/
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
const tiles = selectedOrDefaultLayer.data;
if (typeof tiles === "string") {
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
}
const possibleStartPositions: PositionInterface[] = [];
tiles.forEach((objectKey: number, key: number) => {
if (objectKey === 0) {
return;
}
const y = Math.floor(key / selectedOrDefaultLayer.width);
const x = key % selectedOrDefaultLayer.width;
if (selectedLayer && this.gameMap.hasStartTile) {
const properties = this.gameMap.getPropertiesForIndex(objectKey);
if (
!properties.length ||
!properties.some((property) => property.name == "start" && property.value == selectedLayer)
) {
return;
}
}
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
});
// Get a value at random amongst allowed values
if (possibleStartPositions.length === 0) {
console.warn('The start layer "' + selectedOrDefaultLayer.name + '" for this map is empty.');
return {
x: 0,
y: 0,
};
}
// Choose one of the available start positions at random amongst the list of available start positions.
return possibleStartPositions[Math.floor(Math.random() * possibleStartPositions.length)];
}
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
if (!properties) {
return undefined;
}
const obj = properties.find(
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
);
if (obj === undefined) {
return undefined;
}
return obj.value;
}
}

View File

@ -1,8 +1,8 @@
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import {Scene} from "phaser"; import { Scene } from "phaser";
import {ErrorScene} from "../Reconnecting/ErrorScene"; import { ErrorScene } from "../Reconnecting/ErrorScene";
import {WAError} from "../Reconnecting/WAError"; import { WAError } from "../Reconnecting/WAError";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
export const EntrySceneName = "EntryScene"; export const EntrySceneName = "EntryScene";
@ -13,26 +13,32 @@ export const EntrySceneName = "EntryScene";
export class EntryScene extends Scene { export class EntryScene extends Scene {
constructor() { constructor() {
super({ super({
key: EntrySceneName key: EntrySceneName,
}); });
} }
create() { create() {
gameManager
gameManager.init(this.scene).then((nextSceneName) => { .init(this.scene)
// Let's rescale before starting the game .then((nextSceneName) => {
// We can do it at this stage. // Let's rescale before starting the game
waScaleManager.applyNewSize(); // We can do it at this stage.
this.scene.start(nextSceneName); waScaleManager.applyNewSize();
}).catch((err) => { this.scene.start(nextSceneName);
if (err.response && err.response.status == 404) { })
ErrorScene.showError(new WAError( .catch((err) => {
'Access link incorrect', if (err.response && err.response.status == 404) {
'Could not find map. Please check your access link.', ErrorScene.showError(
'If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com'), this.scene); new WAError(
} else { "Access link incorrect",
ErrorScene.showError(err, this.scene); "Could not find map. Please check your access link.",
} "If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
}); ),
this.scene
);
} else {
ErrorScene.showError(err, this.scene);
}
});
} }
} }

View File

@ -1,25 +1,25 @@
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import Rectangle = Phaser.GameObjects.Rectangle; import Rectangle = Phaser.GameObjects.Rectangle;
import {EnableCameraSceneName} from "./EnableCameraScene"; import { EnableCameraSceneName } from "./EnableCameraScene";
import {CustomizeSceneName} from "./CustomizeScene"; import { CustomizeSceneName } from "./CustomizeScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import {loadAllDefaultModels} from "../Entity/PlayerTexturesLoadingManager"; import { loadAllDefaultModels } from "../Entity/PlayerTexturesLoadingManager";
import {addLoader} from "../Components/Loader"; import { addLoader } from "../Components/Loader";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene"; import { AbstractCharacterScene } from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser"; import { areCharacterLayersValid } from "../../Connexion/LocalUser";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import { touchScreenManager } from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import { PinchManager } from "../UserInput/PinchManager";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore"; import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import {waScaleManager} from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable"; import { isMobile } from "../../Enum/EnvironmentVariable";
//todo: put this constants in a dedicated file //todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene"; export const SelectCharacterSceneName = "SelectCharacterScene";
export class SelectCharacterScene extends AbstractCharacterScene { export class SelectCharacterScene extends AbstractCharacterScene {
protected readonly nbCharactersPerRow = 6; protected readonly nbCharactersPerRow = 6;
protected selectedPlayer!: Phaser.Physics.Arcade.Sprite|null; // null if we are selecting the "customize" option protected selectedPlayer!: Phaser.Physics.Arcade.Sprite | null; // null if we are selecting the "customize" option
protected players: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>(); protected players: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>();
protected playerModels!: BodyResourceDescriptionInterface[]; protected playerModels!: BodyResourceDescriptionInterface[];
@ -38,7 +38,6 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
preload() { preload() {
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => { this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => { bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription); this.playerModels.push(bodyResourceDescription);
@ -54,7 +53,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
create() { create() {
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
this.events.addListener('wake', () => { this.events.addListener("wake", () => {
waScaleManager.saveZoom(); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMobile() ? 2 : 1;
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
@ -68,26 +67,26 @@ export class SelectCharacterScene extends AbstractCharacterScene {
waScaleManager.zoomModifier = isMobile() ? 2 : 1; waScaleManager.zoomModifier = isMobile() ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF); this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff);
this.selectedRectangle.setDepth(2); this.selectedRectangle.setDepth(2);
/*create user*/ /*create user*/
this.createCurrentPlayer(); this.createCurrentPlayer();
this.input.keyboard.on('keyup-ENTER', () => { this.input.keyboard.on("keyup-ENTER", () => {
return this.nextSceneToCameraScene(); return this.nextSceneToCameraScene();
}); });
this.input.keyboard.on('keydown-RIGHT', () => { this.input.keyboard.on("keydown-RIGHT", () => {
this.moveToRight(); this.moveToRight();
}); });
this.input.keyboard.on('keydown-LEFT', () => { this.input.keyboard.on("keydown-LEFT", () => {
this.moveToLeft(); this.moveToLeft();
}); });
this.input.keyboard.on('keydown-UP', () => { this.input.keyboard.on("keydown-UP", () => {
this.moveToUp(); this.moveToUp();
}); });
this.input.keyboard.on('keydown-DOWN', () => { this.input.keyboard.on("keydown-DOWN", () => {
this.moveToDown(); this.moveToDown();
}); });
} }
@ -96,7 +95,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) { if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) {
return; return;
} }
if(!this.selectedPlayer){ if (!this.selectedPlayer) {
return; return;
} }
this.scene.stop(SelectCharacterSceneName); this.scene.stop(SelectCharacterSceneName);
@ -105,7 +104,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
gameManager.tryResumingGame(this, EnableCameraSceneName); gameManager.tryResumingGame(this, EnableCameraSceneName);
this.players = []; this.players = [];
selectCharacterSceneVisibleStore.set(false); selectCharacterSceneVisibleStore.set(false);
this.events.removeListener('wake'); this.events.removeListener("wake");
} }
public nextSceneToCustomizeScene(): void { public nextSceneToCustomizeScene(): void {
@ -119,11 +118,11 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
createCurrentPlayer(): void { createCurrentPlayer(): void {
for (let i = 0; i <this.playerModels.length; i++) { for (let i = 0; i < this.playerModels.length; i++) {
const playerResource = this.playerModels[i]; const playerResource = this.playerModels[i];
//check already exist texture //check already exist texture
if(this.players.find((c) => c.texture.key === playerResource.name)){ if (this.players.find((c) => c.texture.key === playerResource.name)) {
continue; continue;
} }
@ -132,9 +131,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.setUpPlayer(player, i); this.setUpPlayer(player, i);
this.anims.create({ this.anims.create({
key: playerResource.name, key: playerResource.name,
frames: this.anims.generateFrameNumbers(playerResource.name, {start: 0, end: 11}), frames: this.anims.generateFrameNumbers(playerResource.name, { start: 0, end: 11 }),
frameRate: 8, frameRate: 8,
repeat: -1 repeat: -1,
}); });
player.setInteractive().on("pointerdown", () => { player.setInteractive().on("pointerdown", () => {
if (this.pointerClicked) { if (this.pointerClicked) {
@ -153,77 +152,79 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}); });
this.players.push(player); this.players.push(player);
} }
if (this.currentSelectUser >= this.players.length) {
this.currentSelectUser = 0;
}
this.selectedPlayer = this.players[this.currentSelectUser]; this.selectedPlayer = this.players[this.currentSelectUser];
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name); this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
} }
protected moveUser(){ protected moveUser() {
for(let i = 0; i < this.players.length; i++){ for (let i = 0; i < this.players.length; i++) {
const player = this.players[i]; const player = this.players[i];
this.setUpPlayer(player, i); this.setUpPlayer(player, i);
} }
this.updateSelectedPlayer(); this.updateSelectedPlayer();
} }
public moveToLeft(){ public moveToLeft() {
if(this.currentSelectUser === 0){ if (this.currentSelectUser === 0) {
return; return;
} }
this.currentSelectUser -= 1; this.currentSelectUser -= 1;
this.moveUser(); this.moveUser();
} }
public moveToRight(){ public moveToRight() {
if(this.currentSelectUser === (this.players.length - 1)){ if (this.currentSelectUser === this.players.length - 1) {
return; return;
} }
this.currentSelectUser += 1; this.currentSelectUser += 1;
this.moveUser(); this.moveUser();
} }
protected moveToUp(){ protected moveToUp() {
if(this.currentSelectUser < this.nbCharactersPerRow){ if (this.currentSelectUser < this.nbCharactersPerRow) {
return; return;
} }
this.currentSelectUser -= this.nbCharactersPerRow; this.currentSelectUser -= this.nbCharactersPerRow;
this.moveUser(); this.moveUser();
} }
protected moveToDown(){ protected moveToDown() {
if((this.currentSelectUser + this.nbCharactersPerRow) > (this.players.length - 1)){ if (this.currentSelectUser + this.nbCharactersPerRow > this.players.length - 1) {
return; return;
} }
this.currentSelectUser += this.nbCharactersPerRow; this.currentSelectUser += this.nbCharactersPerRow;
this.moveUser(); this.moveUser();
} }
protected defineSetupPlayer(num: number){ protected defineSetupPlayer(num: number) {
const deltaX = 32; const deltaX = 32;
const deltaY = 32; const deltaY = 32;
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (num % this.nbCharactersPerRow)) ); // calcul position on line users playerX = playerX - deltaX * 2.5 + deltaX * (num % this.nbCharactersPerRow); // calcul position on line users
playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(num / this.nbCharactersPerRow) )) ); // calcul position on column users playerY = playerY - deltaY * 2 + deltaY * Math.floor(num / this.nbCharactersPerRow); // calcul position on column users
const playerVisible = true; const playerVisible = true;
const playerScale = 1; const playerScale = 1;
const playerOpacity = 1; const playerOpacity = 1;
// if selected // if selected
if( num === this.currentSelectUser ){ if (num === this.currentSelectUser) {
this.selectedRectangle.setX(playerX); this.selectedRectangle.setX(playerX);
this.selectedRectangle.setY(playerY); this.selectedRectangle.setY(playerY);
} }
return {playerX, playerY, playerScale, playerOpacity, playerVisible} return { playerX, playerY, playerScale, playerOpacity, playerVisible };
} }
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number){ protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number) {
const { playerX, playerY, playerScale, playerOpacity, playerVisible } = this.defineSetupPlayer(num);
const {playerX, playerY, playerScale, playerOpacity, playerVisible} = this.defineSetupPlayer(num);
player.setBounce(0.2); player.setBounce(0.2);
player.setCollideWorldBounds(false); player.setCollideWorldBounds(false);
player.setVisible( playerVisible ); player.setVisible(playerVisible);
player.setScale(playerScale, playerScale); player.setScale(playerScale, playerScale);
player.setAlpha(playerOpacity); player.setAlpha(playerOpacity);
player.setX(playerX); player.setX(playerX);
@ -234,10 +235,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
* Returns pixel position by on column and row number * Returns pixel position by on column and row number
*/ */
protected getCharacterPosition(): [number, number] { protected getCharacterPosition(): [number, number] {
return [ return [this.game.renderer.width / 2, this.game.renderer.height / 2.5];
this.game.renderer.width / 2,
this.game.renderer.height / 2.5
];
} }
protected updateSelectedPlayer(): void { protected updateSelectedPlayer(): void {
@ -256,7 +254,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.pointerClicked = false; this.pointerClicked = false;
} }
if(this.lazyloadingAttempt){ if (this.lazyloadingAttempt) {
//re-render players list //re-render players list
this.createCurrentPlayer(); this.createCurrentPlayer();
this.moveUser(); this.moveUser();

View File

@ -36,7 +36,7 @@ export interface ITiledMap {
export interface ITiledMapLayerProperty { export interface ITiledMapLayerProperty {
name: string; name: string;
type: string; type: string;
value: string|boolean|number|undefined; value: string | boolean | number | undefined;
} }
/*export interface ITiledMapLayerBooleanProperty { /*export interface ITiledMapLayerBooleanProperty {
@ -48,7 +48,7 @@ export interface ITiledMapLayerProperty {
export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer; export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
export interface ITiledMapGroupLayer { export interface ITiledMapGroupLayer {
id?: number, id?: number;
name: string; name: string;
opacity: number; opacity: number;
properties?: ITiledMapLayerProperty[]; properties?: ITiledMapLayerProperty[];
@ -64,8 +64,8 @@ export interface ITiledMapGroupLayer {
} }
export interface ITiledMapTileLayer { export interface ITiledMapTileLayer {
id?: number, id?: number;
data: number[]|string; data: number[] | string;
height: number; height: number;
name: string; name: string;
opacity: number; opacity: number;
@ -87,7 +87,7 @@ export interface ITiledMapTileLayer {
} }
export interface ITiledMapObjectLayer { export interface ITiledMapObjectLayer {
id?: number, id?: number;
height: number; height: number;
name: string; name: string;
opacity: number; opacity: number;
@ -117,7 +117,7 @@ export interface ITiledMapObject {
gid: number; gid: number;
height: number; height: number;
name: string; name: string;
properties: {[key: string]: string}; properties: { [key: string]: string };
rotation: number; rotation: number;
type: string; type: string;
visible: boolean; visible: boolean;
@ -133,26 +133,26 @@ export interface ITiledMapObject {
/** /**
* Polygon points * Polygon points
*/ */
polygon: {x: number, y: number}[]; polygon: { x: number; y: number }[];
/** /**
* Polyline points * Polyline points
*/ */
polyline: {x: number, y: number}[]; polyline: { x: number; y: number }[];
text?: ITiledText text?: ITiledText;
} }
export interface ITiledText { export interface ITiledText {
text: string, text: string;
wrap?: boolean, wrap?: boolean;
fontfamily?: string, fontfamily?: string;
pixelsize?: number, pixelsize?: number;
color?: string, color?: string;
underline?: boolean, underline?: boolean;
italic?: boolean, italic?: boolean;
strikeout?: boolean, strikeout?: boolean;
halign?: "center"|"right"|"justify"|"left" halign?: "center" | "right" | "justify" | "left";
} }
export interface ITiledTileSet { export interface ITiledTileSet {
@ -163,7 +163,7 @@ export interface ITiledTileSet {
imagewidth: number; imagewidth: number;
margin: number; margin: number;
name: string; name: string;
properties: {[key: string]: string}; properties: { [key: string]: string };
spacing: number; spacing: number;
tilecount: number; tilecount: number;
tileheight: number; tileheight: number;
@ -179,10 +179,10 @@ export interface ITiledTileSet {
} }
export interface ITile { export interface ITile {
id: number, id: number;
type?: string type?: string;
properties?: Array<ITiledMapLayerProperty> properties?: Array<ITiledMapLayerProperty>;
} }
export interface ITiledMapTerrain { export interface ITiledMapTerrain {

View File

@ -1,20 +1,20 @@
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap"; import type { ITiledMap, ITiledMapLayer } from "./ITiledMap";
/** /**
* Flatten the grouped layers * Flatten the grouped layers
*/ */
export function flattenGroupLayersMap(map: ITiledMap) { export function flattenGroupLayersMap(map: ITiledMap) {
const flatLayers: ITiledMapLayer[] = []; const flatLayers: ITiledMapLayer[] = [];
flattenGroupLayers(map.layers, '', flatLayers); flattenGroupLayers(map.layers, "", flatLayers);
return flatLayers; return flatLayers;
} }
function flattenGroupLayers(layers : ITiledMapLayer[], prefix : string, flatLayers: ITiledMapLayer[]) { function flattenGroupLayers(layers: ITiledMapLayer[], prefix: string, flatLayers: ITiledMapLayer[]) {
for (const layer of layers) { for (const layer of layers) {
if (layer.type === 'group') { if (layer.type === "group") {
flattenGroupLayers(layer.layers, prefix + layer.name + '/', flatLayers); flattenGroupLayers(layer.layers, prefix + layer.name + "/", flatLayers);
} else { } else {
layer.name = prefix+layer.name layer.name = prefix + layer.name;
flatLayers.push(layer); flatLayers.push(layer);
} }
} }

View File

@ -1,29 +1,32 @@
import {LoginScene, LoginSceneName} from "../Login/LoginScene"; import { LoginScene, LoginSceneName } from "../Login/LoginScene";
import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene"; import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene";
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import {localUserStore} from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu"; import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
import {connectionManager} from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager"; import { GameConnexionTypes } from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore"; import { menuIconVisible } from "../../Stores/MenuStore";
import {videoConstraintStore} from "../../Stores/MediaStore"; import { videoConstraintStore } from "../../Stores/MediaStore";
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
import { HtmlUtils } from '../../WebRtc/HtmlUtils'; import { HtmlUtils } from "../../WebRtc/HtmlUtils";
import { iframeListener } from '../../Api/IframeListener'; import { iframeListener } from "../../Api/IframeListener";
import { Subscription } from 'rxjs'; import { Subscription } from "rxjs";
import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent"; import { registerMenuCommandStream } from "../../Api/Events/ui/MenuItemRegisterEvent";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem"; import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {get} from "svelte/store"; import { get } from "svelte/store";
import { playersStore } from "../../Stores/PlayersStore";
import { mediaManager } from "../../WebRtc/MediaManager";
import { chatVisibilityStore } from "../../Stores/ChatStore";
export const MenuSceneName = 'MenuScene'; export const MenuSceneName = "MenuScene";
const gameMenuKey = 'gameMenu'; const gameMenuKey = "gameMenu";
const gameMenuIconKey = 'gameMenuIcon'; const gameMenuIconKey = "gameMenuIcon";
const gameSettingsMenuKey = 'gameSettingsMenu'; const gameSettingsMenuKey = "gameSettingsMenu";
const gameShare = 'gameShare'; const gameShare = "gameShare";
const closedSideMenuX = -1000; const closedSideMenuX = -1000;
const openedSideMenuX = 0; const openedSideMenuX = 0;
@ -44,45 +47,49 @@ export class MenuScene extends Phaser.Scene {
private menuButton!: Phaser.GameObjects.DOMElement; private menuButton!: Phaser.GameObjects.DOMElement;
private warningContainer: WarningContainer | null = null; private warningContainer: WarningContainer | null = null;
private warningContainerTimeout: NodeJS.Timeout | null = null; private warningContainerTimeout: NodeJS.Timeout | null = null;
private subscriptions = new Subscription() private subscriptions = new Subscription();
constructor() { constructor() {
super({ key: MenuSceneName }); super({ key: MenuSceneName });
this.gameQualityValue = localUserStore.getGameQualityValue(); this.gameQualityValue = localUserStore.getGameQualityValue();
this.videoQualityValue = localUserStore.getVideoQualityValue(); this.videoQualityValue = localUserStore.getVideoQualityValue();
this.subscriptions.add(registerMenuCommandStream.subscribe(menuCommand => { this.subscriptions.add(
this.addMenuOption(menuCommand); registerMenuCommandStream.subscribe((menuCommand) => {
})) this.addMenuOption(menuCommand);
})
);
this.subscriptions.add(iframeListener.unregisterMenuCommandStream.subscribe(menuCommand => { this.subscriptions.add(
this.destroyMenu(menuCommand); iframeListener.unregisterMenuCommandStream.subscribe((menuCommand) => {
})) this.destroyMenu(menuCommand);
})
);
} }
reset() { reset() {
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")]; const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
for (let index = addedMenuItems.length - 1; index >= 0; index--) { for (let index = addedMenuItems.length - 1; index >= 0; index--) {
addedMenuItems[index].remove() addedMenuItems[index].remove();
} }
} }
public addMenuOption(menuText: string) { public addMenuOption(menuText: string) {
const wrappingSection = document.createElement("section") const wrappingSection = document.createElement("section");
const escapedHtml = HtmlUtils.escapeHtml(menuText); const escapedHtml = HtmlUtils.escapeHtml(menuText);
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>` wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`;
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main"); const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
if (menuItemContainer) { if (menuItemContainer) {
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove() menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove();
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks")) menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"));
} }
} }
preload() { preload() {
this.load.html(gameMenuKey, 'resources/html/gameMenu.html'); this.load.html(gameMenuKey, "resources/html/gameMenu.html");
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); this.load.html(gameMenuIconKey, "resources/html/gameMenuIcon.html");
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
this.load.html(gameShare, 'resources/html/gameShare.html'); this.load.html(gameShare, "resources/html/gameShare.html");
this.load.html(gameReportKey, gameReportRessource); this.load.html(gameReportKey, gameReportRessource);
this.load.html(warningContainerKey, warningContainerHtml); this.load.html(warningContainerKey, warningContainerHtml);
} }
@ -91,46 +98,59 @@ export class MenuScene extends Phaser.Scene {
menuIconVisible.set(true); menuIconVisible.set(true);
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey); this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
this.menuElement.setOrigin(0); this.menuElement.setOrigin(0);
MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu'); MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
const middleX = (window.innerWidth / 3) - 298; if (mediaManager.hasNotification()) {
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
}
const middleX = window.innerWidth / 3 - 298;
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey); this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality'); MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare); this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare); MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
this.gameShareElement.addListener('click'); this.gameShareElement.addListener("click");
this.gameShareElement.on('click', (event: MouseEvent) => { this.gameShareElement.on("click", (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') { if ((event?.target as HTMLInputElement).id === "gameShareFormSubmit") {
this.copyLink(); this.copyLink();
} else if ((event?.target as HTMLInputElement).id === 'gameShareFormCancel') { } else if ((event?.target as HTMLInputElement).id === "gameShareFormCancel") {
this.closeGameShare(); this.closeGameShare();
} }
}); });
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous); this.gameReportElement = new ReportMenu(
this,
connectionManager.getConnexionType === GameConnexionTypes.anonymous
);
showReportScreenStore.subscribe((user) => { showReportScreenStore.subscribe((user) => {
if (user !== null) { if (user !== null) {
this.closeAll(); this.closeAll();
this.gameReportElement.open(user.userId, user.userName); const uuid = playersStore.getPlayerById(user.userId)?.userUuid;
if (uuid === undefined) {
throw new Error("Could not find UUID for user with ID " + user.userId);
}
this.gameReportElement.open(uuid, user.userName);
} }
}); });
this.input.keyboard.on('keyup-TAB', () => { this.input.keyboard.on("keyup-TAB", () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
}); });
this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey); this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey);
this.menuButton.addListener('click'); this.menuButton.addListener("click");
this.menuButton.on('click', () => { this.menuButton.on("click", () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
}); });
this.menuElement.addListener('click'); this.menuElement.addListener("click");
this.menuElement.on('click', this.onMenuClick.bind(this)); this.menuElement.on("click", this.onMenuClick.bind(this));
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning()); worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
chatVisibilityStore.subscribe((v) => {
this.menuButton.setVisible(!v);
});
} }
//todo put this method in a parent menuElement class //todo put this method in a parent menuElement class
@ -145,7 +165,7 @@ export class MenuScene extends Phaser.Scene {
public revealMenuIcon(): void { public revealMenuIcon(): void {
//TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function //TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function
try { try {
(this.menuButton.getChildByID('menuIcon') as HTMLElement).hidden = false; (this.menuButton.getChildByID("menuIcon") as HTMLElement).hidden = false;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -155,22 +175,22 @@ export class MenuScene extends Phaser.Scene {
if (this.sideMenuOpened) return; if (this.sideMenuOpened) return;
this.closeAll(); this.closeAll();
this.sideMenuOpened = true; this.sideMenuOpened = true;
this.menuButton.getChildByID('openMenuButton').innerHTML = 'X'; this.menuButton.getChildByID("openMenuButton").innerHTML = "X";
const connection = gameManager.getCurrentGameScene(this).connection; const connection = gameManager.getCurrentGameScene(this).connection;
if (connection && connection.isAdmin()) { if (connection && connection.isAdmin()) {
const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement; const adminSection = this.menuElement.getChildByID("adminConsoleSection") as HTMLElement;
adminSection.hidden = false; adminSection.hidden = false;
} }
//TODO bind with future metadata of card //TODO bind with future metadata of card
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){ //if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement; const adminSection = this.menuElement.getChildByID("socialLinks") as HTMLElement;
adminSection.hidden = false; adminSection.hidden = false;
//} //}
this.tweens.add({ this.tweens.add({
targets: this.menuElement, targets: this.menuElement,
x: openedSideMenuX, x: openedSideMenuX,
duration: 500, duration: 500,
ease: 'Power3' ease: "Power3",
}); });
} }
@ -183,23 +203,22 @@ export class MenuScene extends Phaser.Scene {
} }
this.warningContainerTimeout = setTimeout(() => { this.warningContainerTimeout = setTimeout(() => {
this.warningContainer?.destroy(); this.warningContainer?.destroy();
this.warningContainer = null this.warningContainer = null;
this.warningContainerTimeout = null this.warningContainerTimeout = null;
}, 120000); }, 120000);
} }
private closeSideMenu(): void { private closeSideMenu(): void {
if (!this.sideMenuOpened) return; if (!this.sideMenuOpened) return;
this.sideMenuOpened = false; this.sideMenuOpened = false;
this.closeAll(); this.closeAll();
this.menuButton.getChildByID('openMenuButton').innerHTML = `<img src="/static/images/menu.svg">`; this.menuButton.getChildByID("openMenuButton").innerHTML = `<img src="/static/images/menu.svg">`;
consoleGlobalMessageManagerVisibleStore.set(false); consoleGlobalMessageManagerVisibleStore.set(false);
this.tweens.add({ this.tweens.add({
targets: this.menuElement, targets: this.menuElement,
x: closedSideMenuX, x: closedSideMenuX,
duration: 500, duration: 500,
ease: 'Power3' ease: "Power3",
}); });
} }
@ -213,19 +232,23 @@ export class MenuScene extends Phaser.Scene {
this.settingsMenuOpened = true; this.settingsMenuOpened = true;
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; const gameQualitySelect = this.gameQualityMenuElement.getChildByID("select-game-quality") as HTMLInputElement;
gameQualitySelect.value = '' + this.gameQualityValue; gameQualitySelect.value = "" + this.gameQualityValue;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; const videoQualitySelect = this.gameQualityMenuElement.getChildByID("select-video-quality") as HTMLInputElement;
videoQualitySelect.value = '' + this.videoQualityValue; videoQualitySelect.value = "" + this.videoQualityValue;
this.gameQualityMenuElement.addListener('click'); this.gameQualityMenuElement.addListener("click");
this.gameQualityMenuElement.on('click', (event: MouseEvent) => { this.gameQualityMenuElement.on("click", (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') { if ((event?.target as HTMLInputElement).id === "gameQualityFormSubmit") {
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; const gameQualitySelect = this.gameQualityMenuElement.getChildByID(
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; "select-game-quality"
) as HTMLInputElement;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID(
"select-video-quality"
) as HTMLInputElement;
this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value)); this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value));
} else if ((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') { } else if ((event?.target as HTMLInputElement).id === "gameQualityFormCancel") {
this.closeGameQualityMenu(); this.closeGameQualityMenu();
} }
}); });
@ -243,7 +266,7 @@ export class MenuScene extends Phaser.Scene {
y: middleY, y: middleY,
x: middleX, x: middleX,
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
@ -251,16 +274,15 @@ export class MenuScene extends Phaser.Scene {
if (!this.settingsMenuOpened) return; if (!this.settingsMenuOpened) return;
this.settingsMenuOpened = false; this.settingsMenuOpened = false;
this.gameQualityMenuElement.removeListener('click'); this.gameQualityMenuElement.removeListener("click");
this.tweens.add({ this.tweens.add({
targets: this.gameQualityMenuElement, targets: this.gameQualityMenuElement,
y: -400, y: -400,
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
private openGameShare(): void { private openGameShare(): void {
if (this.gameShareOpened) { if (this.gameShareOpened) {
this.closeGameShare(); this.closeGameShare();
@ -269,7 +291,7 @@ export class MenuScene extends Phaser.Scene {
//close all //close all
this.closeAll(); this.closeAll();
const gameShareLink = this.gameShareElement.getChildByID('gameShareLink') as HTMLInputElement; const gameShareLink = this.gameShareElement.getChildByID("gameShareLink") as HTMLInputElement;
gameShareLink.value = location.toString(); gameShareLink.value = location.toString();
this.gameShareOpened = true; this.gameShareOpened = true;
@ -287,64 +309,67 @@ export class MenuScene extends Phaser.Scene {
y: middleY, y: middleY,
x: middleX, x: middleX,
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
private closeGameShare(): void { private closeGameShare(): void {
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement; const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
gameShareInfo.innerText = ''; gameShareInfo.innerText = "";
gameShareInfo.style.display = 'none'; gameShareInfo.style.display = "none";
this.gameShareOpened = false; this.gameShareOpened = false;
this.tweens.add({ this.tweens.add({
targets: this.gameShareElement, targets: this.gameShareElement,
y: -400, y: -400,
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
private onMenuClick(event: MouseEvent) { private onMenuClick(event: MouseEvent) {
const htmlMenuItem = (event?.target as HTMLInputElement); const htmlMenuItem = event?.target as HTMLInputElement;
if (htmlMenuItem.classList.contains('not-button')) { if (htmlMenuItem.classList.contains("not-button")) {
return; return;
} }
event.preventDefault(); event.preventDefault();
if (htmlMenuItem.classList.contains("fromApi")) { if (htmlMenuItem.classList.contains("fromApi")) {
sendMenuClickedEvent(htmlMenuItem.id) sendMenuClickedEvent(htmlMenuItem.id);
return return;
} }
switch ((event?.target as HTMLInputElement).id) { switch ((event?.target as HTMLInputElement).id) {
case 'changeNameButton': case "changeNameButton":
this.closeSideMenu(); this.closeSideMenu();
gameManager.leaveGame(this, LoginSceneName, new LoginScene()); gameManager.leaveGame(this, LoginSceneName, new LoginScene());
break; break;
case 'sparkButton': case "sparkButton":
this.gotToCreateMapPage(); this.gotToCreateMapPage();
break; break;
case 'changeSkinButton': case "changeSkinButton":
this.closeSideMenu(); this.closeSideMenu();
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene()); gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
break; break;
case 'changeCompanionButton': case "changeCompanionButton":
this.closeSideMenu(); this.closeSideMenu();
gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene()); gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene());
break; break;
case 'closeButton': case "closeButton":
this.closeSideMenu(); this.closeSideMenu();
break; break;
case 'shareButton': case "shareButton":
this.openGameShare(); this.openGameShare();
break; break;
case 'editGameSettingsButton': case "editGameSettingsButton":
this.openGameSettingsMenu(); this.openGameSettingsMenu();
break; break;
case 'toggleFullscreen': case "toggleFullscreen":
this.toggleFullscreen(); this.toggleFullscreen();
break; break;
case 'adminConsoleButton': case "enableNotification":
this.enableNotification();
break;
case "adminConsoleButton":
if (get(consoleGlobalMessageManagerVisibleStore)) { if (get(consoleGlobalMessageManagerVisibleStore)) {
consoleGlobalMessageManagerVisibleStore.set(false); consoleGlobalMessageManagerVisibleStore.set(false);
} else { } else {
@ -356,9 +381,9 @@ export class MenuScene extends Phaser.Scene {
private async copyLink() { private async copyLink() {
await navigator.clipboard.writeText(location.toString()); await navigator.clipboard.writeText(location.toString());
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement; const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
gameShareInfo.innerText = 'Link copied, you can share it now!'; gameShareInfo.innerText = "Link copied, you can share it now!";
gameShareInfo.style.display = 'block'; gameShareInfo.style.display = "block";
} }
private saveSetting(valueGame: number, valueVideo: number) { private saveSetting(valueGame: number, valueVideo: number) {
@ -378,8 +403,8 @@ export class MenuScene extends Phaser.Scene {
private gotToCreateMapPage() { private gotToCreateMapPage() {
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html'; //const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
//TODO fix me: this button can to send us on WorkAdventure BO. //TODO fix me: this button can to send us on WorkAdventure BO.
const sparkHost = 'https://workadventu.re/getting-started'; const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, '_blank'); window.open(sparkHost, "_blank");
} }
private closeAll() { private closeAll() {
@ -389,10 +414,10 @@ export class MenuScene extends Phaser.Scene {
} }
private toggleFullscreen() { private toggleFullscreen() {
const body = document.querySelector('body') const body = document.querySelector("body");
if (body) { if (body) {
if (document.fullscreenElement ?? document.fullscreen) { if (document.fullscreenElement ?? document.fullscreen) {
document.exitFullscreen() document.exitFullscreen();
} else { } else {
body.requestFullscreen(); body.requestFullscreen();
} }
@ -406,4 +431,12 @@ export class MenuScene extends Phaser.Scene {
public isDirty(): boolean { public isDirty(): boolean {
return false; return false;
} }
private enableNotification() {
mediaManager.requestNotification().then(() => {
if (mediaManager.hasNotification()) {
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
}
});
}
} }

View File

@ -1,15 +1,16 @@
import {MenuScene} from "./MenuScene"; import { MenuScene } from "./MenuScene";
import {gameManager} from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import {blackListManager} from "../../WebRtc/BlackListManager"; import { blackListManager } from "../../WebRtc/BlackListManager";
import { playersStore } from "../../Stores/PlayersStore";
export const gameReportKey = 'gameReport'; export const gameReportKey = "gameReport";
export const gameReportRessource = 'resources/html/gameReport.html'; export const gameReportRessource = "resources/html/gameReport.html";
export class ReportMenu extends Phaser.GameObjects.DOMElement { export class ReportMenu extends Phaser.GameObjects.DOMElement {
private opened: boolean = false; private opened: boolean = false;
private userId!: number; private userUuid!: string;
private userName!: string|undefined; private userName!: string | undefined;
private anonymous: boolean; private anonymous: boolean;
constructor(scene: Phaser.Scene, anonymous: boolean) { constructor(scene: Phaser.Scene, anonymous: boolean) {
@ -18,46 +19,46 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
this.createFromCache(gameReportKey); this.createFromCache(gameReportKey);
if (this.anonymous) { if (this.anonymous) {
const divToHide = this.getChildByID('reportSection') as HTMLElement; const divToHide = this.getChildByID("reportSection") as HTMLElement;
divToHide.hidden = true; divToHide.hidden = true;
const textToHide = this.getChildByID('askActionP') as HTMLElement; const textToHide = this.getChildByID("askActionP") as HTMLElement;
textToHide.hidden = true; textToHide.hidden = true;
} }
scene.add.existing(this); scene.add.existing(this);
MenuScene.revealMenusAfterInit(this, gameReportKey); MenuScene.revealMenusAfterInit(this, gameReportKey);
this.addListener('click'); this.addListener("click");
this.on('click', (event:MouseEvent) => { this.on("click", (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameReportFormSubmit') { if ((event?.target as HTMLInputElement).id === "gameReportFormSubmit") {
this.submitReport(); this.submitReport();
} else if((event?.target as HTMLInputElement).id === 'gameReportFormCancel') { } else if ((event?.target as HTMLInputElement).id === "gameReportFormCancel") {
this.close(); this.close();
} else if((event?.target as HTMLInputElement).id === 'toggleBlockButton') { } else if ((event?.target as HTMLInputElement).id === "toggleBlockButton") {
this.toggleBlock(); this.toggleBlock();
} }
}); });
} }
public open(userId: number, userName: string|undefined): void { public open(userUuid: string, userName: string | undefined): void {
if (this.opened) { if (this.opened) {
this.close(); this.close();
return; return;
} }
this.userId = userId; this.userUuid = userUuid;
this.userName = userName; this.userName = userName;
const mainEl = this.getChildByID('gameReport') as HTMLElement; const mainEl = this.getChildByID("gameReport") as HTMLElement;
this.x = this.getCenteredX(mainEl); this.x = this.getCenteredX(mainEl);
this.y = this.getHiddenY(mainEl); this.y = this.getHiddenY(mainEl);
const gameTitleReport = this.getChildByID('nameReported') as HTMLElement; const gameTitleReport = this.getChildByID("nameReported") as HTMLElement;
gameTitleReport.innerText = userName || ''; gameTitleReport.innerText = userName || "";
const blockButton = this.getChildByID('toggleBlockButton') as HTMLElement; const blockButton = this.getChildByID("toggleBlockButton") as HTMLElement;
blockButton.innerText = blackListManager.isBlackListed(this.userId) ? 'Unblock this user' : 'Block this user'; blockButton.innerText = blackListManager.isBlackListed(this.userUuid) ? "Unblock this user" : "Block this user";
this.opened = true; this.opened = true;
@ -67,19 +68,19 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
targets: this, targets: this,
y: this.getCenteredY(mainEl), y: this.getCenteredY(mainEl),
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
public close(): void { public close(): void {
gameManager.getCurrentGameScene(this.scene).userInputManager.restoreControls(); gameManager.getCurrentGameScene(this.scene).userInputManager.restoreControls();
this.opened = false; this.opened = false;
const mainEl = this.getChildByID('gameReport') as HTMLElement; const mainEl = this.getChildByID("gameReport") as HTMLElement;
this.scene.tweens.add({ this.scene.tweens.add({
targets: this, targets: this,
y: this.getHiddenY(mainEl), y: this.getHiddenY(mainEl),
duration: 1000, duration: 1000,
ease: 'Power3' ease: "Power3",
}); });
} }
@ -88,31 +89,32 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
return window.innerWidth / 4 - mainEl.clientWidth / 2; return window.innerWidth / 4 - mainEl.clientWidth / 2;
} }
private getHiddenY(mainEl: HTMLElement): number { private getHiddenY(mainEl: HTMLElement): number {
return - mainEl.clientHeight - 50; return -mainEl.clientHeight - 50;
} }
private getCenteredY(mainEl: HTMLElement): number { private getCenteredY(mainEl: HTMLElement): number {
return window.innerHeight / 4 - mainEl.clientHeight / 2; return window.innerHeight / 4 - mainEl.clientHeight / 2;
} }
private toggleBlock(): void { private toggleBlock(): void {
!blackListManager.isBlackListed(this.userId) ? blackListManager.blackList(this.userId) : blackListManager.cancelBlackList(this.userId); !blackListManager.isBlackListed(this.userUuid)
? blackListManager.blackList(this.userUuid)
: blackListManager.cancelBlackList(this.userUuid);
this.close(); this.close();
} }
private submitReport(): void{ private submitReport(): void {
const gamePError = this.getChildByID('gameReportErr') as HTMLParagraphElement; const gamePError = this.getChildByID("gameReportErr") as HTMLParagraphElement;
gamePError.innerText = ''; gamePError.innerText = "";
gamePError.style.display = 'none'; gamePError.style.display = "none";
const gameTextArea = this.getChildByID('gameReportInput') as HTMLInputElement; const gameTextArea = this.getChildByID("gameReportInput") as HTMLInputElement;
if(!gameTextArea || !gameTextArea.value){ if (!gameTextArea || !gameTextArea.value) {
gamePError.innerText = 'Report message cannot to be empty.'; gamePError.innerText = "Report message cannot to be empty.";
gamePError.style.display = 'block'; gamePError.style.display = "block";
return; return;
} }
gameManager.getCurrentGameScene(this.scene).connection?.emitReportPlayerMessage( gameManager
this.userId, .getCurrentGameScene(this.scene)
gameTextArea.value .connection?.emitReportPlayerMessage(this.userUuid, gameTextArea.value);
);
this.close(); this.close();
} }
} }

View File

@ -34,7 +34,7 @@ export class ErrorScene extends Phaser.Scene {
} }
preload() { preload() {
this.load.image(Textures.icon, "resources/logos/tcm_full.png"); this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(Textures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); this.load.bitmapFont(Textures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.load.spritesheet( this.load.spritesheet(

View File

@ -19,7 +19,7 @@ export class ReconnectingScene extends Phaser.Scene {
} }
preload() { preload() {
this.load.image(ReconnectingTextures.icon, "resources/logos/tcm_full.png"); this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.load.spritesheet( this.load.spritesheet(

View File

@ -1,16 +1,16 @@
import {get, writable} from "svelte/store"; import { get, writable } from "svelte/store";
import type {Box} from "../WebRtc/LayoutManager"; import type { Box } from "../WebRtc/LayoutManager";
import {HtmlUtils} from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import {LayoutMode} from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
import {layoutModeStore} from "./StreamableCollectionStore"; import { layoutModeStore } from "./StreamableCollectionStore";
/** /**
* Tries to find the biggest available box of remaining space (this is a space where we can center the character) * Tries to find the biggest available box of remaining space (this is a space where we can center the character)
*/ */
function findBiggestAvailableArea(): Box { function findBiggestAvailableArea(): Box {
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas'); const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>("#game canvas");
if (get(layoutModeStore) === LayoutMode.VideoChat) { if (get(layoutModeStore) === LayoutMode.VideoChat) {
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div'); const children = document.querySelectorAll<HTMLDivElement>("div.chat-mode > div");
const htmlChildren = Array.from(children.values()); const htmlChildren = Array.from(children.values());
// No chat? Let's go full center // No chat? Let's go full center
@ -19,18 +19,17 @@ function findBiggestAvailableArea(): Box {
xStart: 0, xStart: 0,
yStart: 0, yStart: 0,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
const lastDiv = htmlChildren[htmlChildren.length - 1]; const lastDiv = htmlChildren[htmlChildren.length - 1];
// Compute area between top right of the last div and bottom right of window // Compute area between top right of the last div and bottom right of window
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) const area1 =
* (game.offsetHeight - lastDiv.offsetTop); (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) * (game.offsetHeight - lastDiv.offsetTop);
// Compute area between bottom of last div and bottom of the screen on whole width // Compute area between bottom of last div and bottom of the screen on whole width
const area2 = game.offsetWidth const area2 = game.offsetWidth * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
* (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
if (area1 < 0 && area2 < 0) { if (area1 < 0 && area2 < 0) {
// If screen is full, let's not attempt something foolish and simply center character in the middle. // If screen is full, let's not attempt something foolish and simply center character in the middle.
@ -38,28 +37,30 @@ function findBiggestAvailableArea(): Box {
xStart: 0, xStart: 0,
yStart: 0, yStart: 0,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
if (area1 <= area2) { if (area1 <= area2) {
return { return {
xStart: 0, xStart: 0,
yStart: lastDiv.offsetTop + lastDiv.offsetHeight, yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} else { } else {
return { return {
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth, xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
yStart: lastDiv.offsetTop, yStart: lastDiv.offsetTop,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
} else { } else {
// Possible destinations: at the center bottom or at the right bottom. // Possible destinations: at the center bottom or at the right bottom.
const mainSectionChildren = Array.from(document.querySelectorAll<HTMLDivElement>('div.main-section > div').values()); const mainSectionChildren = Array.from(
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('aside.sidebar > div').values()); document.querySelectorAll<HTMLDivElement>("div.main-section > div").values()
);
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>("aside.sidebar > div").values());
// No presentation? Let's center on the screen // No presentation? Let's center on the screen
if (mainSectionChildren.length === 0) { if (mainSectionChildren.length === 0) {
@ -67,60 +68,58 @@ function findBiggestAvailableArea(): Box {
xStart: 0, xStart: 0,
yStart: 0, yStart: 0,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
// At this point, we know we have at least one element in the main section. // At this point, we know we have at least one element in the main section.
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1]; const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length - 1];
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) const presentationArea =
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth); (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) *
(lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
let leftSideBar: number; let leftSideBar: number;
let bottomSideBar: number; let bottomSideBar: number;
if (sidebarChildren.length === 0) { if (sidebarChildren.length === 0) {
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').offsetLeft; leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("sidebar").offsetLeft;
bottomSideBar = 0; bottomSideBar = 0;
} else { } else {
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1]; const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
leftSideBar = lastSideBarChildren.offsetLeft; leftSideBar = lastSideBarChildren.offsetLeft;
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight; bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
} }
const sideBarArea = (game.offsetWidth - leftSideBar) const sideBarArea = (game.offsetWidth - leftSideBar) * (game.offsetHeight - bottomSideBar);
* (game.offsetHeight - bottomSideBar);
if (presentationArea <= sideBarArea) { if (presentationArea <= sideBarArea) {
return { return {
xStart: leftSideBar, xStart: leftSideBar,
yStart: bottomSideBar, yStart: bottomSideBar,
xEnd: game.offsetWidth, xEnd: game.offsetWidth,
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} else { } else {
return { return {
xStart: 0, xStart: 0,
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight, yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth, // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
yEnd: game.offsetHeight yEnd: game.offsetHeight,
} };
} }
} }
} }
/** /**
* A store that contains the list of (video) peers we are connected to. * A store that contains the list of (video) peers we are connected to.
*/ */
function createBiggestAvailableAreaStore() { function createBiggestAvailableAreaStore() {
const { subscribe, set } = writable<Box>({ xStart: 0, yStart: 0, xEnd: 1, yEnd: 1 });
const { subscribe, set } = writable<Box>({xStart:0, yStart: 0, xEnd: 1, yEnd: 1});
return { return {
subscribe, subscribe,
recompute: () => { recompute: () => {
set(findBiggestAvailableArea()); set(findBiggestAvailableArea());
} },
}; };
} }

View File

@ -0,0 +1,119 @@
import { writable } from "svelte/store";
import { playersStore } from "./PlayersStore";
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
export const chatVisibilityStore = writable(false);
export const chatInputFocusStore = writable(false);
export const newChatMessageStore = writable<string | null>(null);
export enum ChatMessageTypes {
text = 1,
me,
userIncoming,
userOutcoming,
}
export interface ChatMessage {
type: ChatMessageTypes;
date: Date;
author?: PlayerInterface;
targets?: PlayerInterface[];
text?: string[];
}
function getAuthor(authorId: number): PlayerInterface {
const author = playersStore.getPlayerById(authorId);
if (!author) {
throw "Could not find data for author " + authorId;
}
return author;
}
function createChatMessagesStore() {
const { subscribe, update } = writable<ChatMessage[]>([]);
return {
subscribe,
addIncomingUser(authorId: number) {
update((list) => {
const lastMessage = list[list.length - 1];
if (lastMessage && lastMessage.type === ChatMessageTypes.userIncoming && lastMessage.targets) {
lastMessage.targets.push(getAuthor(authorId));
} else {
list.push({
type: ChatMessageTypes.userIncoming,
targets: [getAuthor(authorId)],
date: new Date(),
});
}
return list;
});
},
addOutcomingUser(authorId: number) {
update((list) => {
const lastMessage = list[list.length - 1];
if (lastMessage && lastMessage.type === ChatMessageTypes.userOutcoming && lastMessage.targets) {
lastMessage.targets.push(getAuthor(authorId));
} else {
list.push({
type: ChatMessageTypes.userOutcoming,
targets: [getAuthor(authorId)],
date: new Date(),
});
}
return list;
});
},
addPersonnalMessage(text: string) {
newChatMessageStore.set(text);
update((list) => {
const lastMessage = list[list.length - 1];
if (lastMessage && lastMessage.type === ChatMessageTypes.me && lastMessage.text) {
lastMessage.text.push(text);
} else {
list.push({
type: ChatMessageTypes.me,
text: [text],
date: new Date(),
});
}
return list;
});
},
addExternalMessage(authorId: number, text: string) {
update((list) => {
const lastMessage = list[list.length - 1];
if (lastMessage && lastMessage.type === ChatMessageTypes.text && lastMessage.text) {
lastMessage.text.push(text);
} else {
list.push({
type: ChatMessageTypes.text,
text: [text],
author: getAuthor(authorId),
date: new Date(),
});
}
return list;
});
chatVisibilityStore.set(true);
},
};
}
export const chatMessagesStore = createChatMessagesStore();
function createChatSubMenuVisibilityStore() {
const { subscribe, update } = writable<string>("");
return {
subscribe,
openSubMenu(playerName: string, index: number) {
const id = playerName + index;
update((oldValue) => {
return oldValue === id ? "" : id;
});
},
};
}
export const chatSubMenuVisbilityStore = createChatSubMenuVisibilityStore();

View File

@ -1,4 +1,4 @@
import {writable} from "svelte/store"; import { writable } from "svelte/store";
/** /**
* A store that contains whether the game overlay is shown or not. * A store that contains whether the game overlay is shown or not.

View File

@ -1,14 +1,14 @@
import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
import {localUserStore} from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
import {userMovingStore} from "./GameStore"; import { userMovingStore } from "./GameStore";
import {HtmlUtils} from "../WebRtc/HtmlUtils"; import { HtmlUtils } from "../WebRtc/HtmlUtils";
import {BrowserTooOldError} from "./Errors/BrowserTooOldError"; import { BrowserTooOldError } from "./Errors/BrowserTooOldError";
import {errorStore} from "./ErrorStore"; import { errorStore } from "./ErrorStore";
import {isIOS} from "../WebRtc/DeviceUtils"; import { isIOS } from "../WebRtc/DeviceUtils";
import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS"; import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS";
import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import {peerStore} from "./PeerStore"; import { peerStore } from "./PeerStore";
import {privacyShutdownStore} from "./PrivacyShutdownStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore";
/** /**
* A store that contains the camera state requested by the user (on or off). * A store that contains the camera state requested by the user (on or off).
@ -57,7 +57,7 @@ export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilit
* A store containing whether the webcam was enabled in the last 10 seconds * A store containing whether the webcam was enabled in the last 10 seconds
*/ */
const enabledWebCam10secondsAgoStore = readable(false, function start(set) { const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
let timeout: NodeJS.Timeout|null = null; let timeout: NodeJS.Timeout | null = null;
const unsubscribe = requestedCameraState.subscribe((enabled) => { const unsubscribe = requestedCameraState.subscribe((enabled) => {
if (enabled === true) { if (enabled === true) {
@ -71,7 +71,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
} else { } else {
set(false); set(false);
} }
}) });
return function stop() { return function stop() {
unsubscribe(); unsubscribe();
@ -82,7 +82,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
* A store containing whether the webcam was enabled in the last 5 seconds * A store containing whether the webcam was enabled in the last 5 seconds
*/ */
const userMoved5SecondsAgoStore = readable(false, function start(set) { const userMoved5SecondsAgoStore = readable(false, function start(set) {
let timeout: NodeJS.Timeout|null = null; let timeout: NodeJS.Timeout | null = null;
const unsubscribe = userMovingStore.subscribe((moving) => { const unsubscribe = userMovingStore.subscribe((moving) => {
if (moving === true) { if (moving === true) {
@ -94,45 +94,51 @@ const userMoved5SecondsAgoStore = readable(false, function start(set) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
set(false); set(false);
}, 5000); }, 5000);
} }
}) });
return function stop() { return function stop() {
unsubscribe(); unsubscribe();
}; };
}); });
/** /**
* A store containing whether the mouse is getting close the bottom right corner. * A store containing whether the mouse is getting close the bottom right corner.
*/ */
const mouseInBottomRight = readable(false, function start(set) { const mouseInBottomRight = readable(false, function start(set) {
let lastInBottomRight = false; let lastInBottomRight = false;
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game'); const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game");
const detectInBottomRight = (event: MouseEvent) => { const detectInBottomRight = (event: MouseEvent) => {
const rect = gameDiv.getBoundingClientRect(); const rect = gameDiv.getBoundingClientRect();
const inBottomRight = event.x - rect.left > rect.width * 3 / 4 && event.y - rect.top > rect.height * 3 / 4; const inBottomRight = event.x - rect.left > (rect.width * 3) / 4 && event.y - rect.top > (rect.height * 3) / 4;
if (inBottomRight !== lastInBottomRight) { if (inBottomRight !== lastInBottomRight) {
lastInBottomRight = inBottomRight; lastInBottomRight = inBottomRight;
set(inBottomRight); set(inBottomRight);
} }
}; };
document.addEventListener('mousemove', detectInBottomRight); document.addEventListener("mousemove", detectInBottomRight);
return function stop() { return function stop() {
document.removeEventListener('mousemove', detectInBottomRight); document.removeEventListener("mousemove", detectInBottomRight);
} };
}); });
/** /**
* A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation. * A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation.
*/ */
export const cameraEnergySavingStore = derived([userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight], ([$userMoved5SecondsAgoStore,$peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => { export const cameraEnergySavingStore = derived(
return !$mouseInBottomRight && !$userMoved5SecondsAgoStore && $peerStore.size === 0 && !$enabledWebCam10secondsAgoStore; [userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight],
}); ([$userMoved5SecondsAgoStore, $peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => {
return (
!$mouseInBottomRight &&
!$userMoved5SecondsAgoStore &&
$peerStore.size === 0 &&
!$enabledWebCam10secondsAgoStore
);
}
);
/** /**
* A store that contains video constraints. * A store that contains video constraints.
@ -143,28 +149,30 @@ function createVideoConstraintStore() {
height: { min: 400, ideal: 720 }, height: { min: 400, ideal: 720 },
frameRate: { ideal: localUserStore.getVideoQualityValue() }, frameRate: { ideal: localUserStore.getVideoQualityValue() },
facingMode: "user", facingMode: "user",
resizeMode: 'crop-and-scale', resizeMode: "crop-and-scale",
aspectRatio: 1.777777778 aspectRatio: 1.777777778,
} as MediaTrackConstraints); } as MediaTrackConstraints);
return { return {
subscribe, subscribe,
setDeviceId: (deviceId: string|undefined) => update((constraints) => { setDeviceId: (deviceId: string | undefined) =>
if (deviceId !== undefined) { update((constraints) => {
constraints.deviceId = { if (deviceId !== undefined) {
exact: deviceId constraints.deviceId = {
}; exact: deviceId,
} else { };
delete constraints.deviceId; } else {
} delete constraints.deviceId;
}
return constraints; return constraints;
}), }),
setFrameRate: (frameRate: number) => update((constraints) => { setFrameRate: (frameRate: number) =>
constraints.frameRate = { ideal: frameRate }; update((constraints) => {
constraints.frameRate = { ideal: frameRate };
return constraints; return constraints;
}) }),
}; };
} }
@ -178,39 +186,39 @@ function createAudioConstraintStore() {
//TODO: make these values configurable in the game settings menu and store them in localstorage //TODO: make these values configurable in the game settings menu and store them in localstorage
autoGainControl: false, autoGainControl: false,
echoCancellation: true, echoCancellation: true,
noiseSuppression: true noiseSuppression: true,
} as boolean|MediaTrackConstraints); } as boolean | MediaTrackConstraints);
let selectedDeviceId = null; let selectedDeviceId = null;
return { return {
subscribe, subscribe,
setDeviceId: (deviceId: string|undefined) => update((constraints) => { setDeviceId: (deviceId: string | undefined) =>
selectedDeviceId = deviceId; update((constraints) => {
selectedDeviceId = deviceId;
if (typeof(constraints) === 'boolean') { if (typeof constraints === "boolean") {
constraints = {} constraints = {};
} }
if (deviceId !== undefined) { if (deviceId !== undefined) {
constraints.deviceId = { constraints.deviceId = {
exact: selectedDeviceId exact: selectedDeviceId,
}; };
} else { } else {
delete constraints.deviceId; delete constraints.deviceId;
} }
return constraints; return constraints;
}) }),
}; };
} }
export const audioConstraintStore = createAudioConstraintStore(); export const audioConstraintStore = createAudioConstraintStore();
let timeout: NodeJS.Timeout; let timeout: NodeJS.Timeout;
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false; let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false; let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
/** /**
* A store containing the media constraints we want to apply. * A store containing the media constraints we want to apply.
@ -225,7 +233,8 @@ export const mediaStreamConstraintsStore = derived(
audioConstraintStore, audioConstraintStore,
privacyShutdownStore, privacyShutdownStore,
cameraEnergySavingStore, cameraEnergySavingStore,
], ( ],
(
[ [
$requestedCameraState, $requestedCameraState,
$requestedMicrophoneState, $requestedMicrophoneState,
@ -235,92 +244,97 @@ export const mediaStreamConstraintsStore = derived(
$audioConstraintStore, $audioConstraintStore,
$privacyShutdownStore, $privacyShutdownStore,
$cameraEnergySavingStore, $cameraEnergySavingStore,
], set ],
set
) => { ) => {
let currentVideoConstraint: boolean | MediaTrackConstraints = $videoConstraintStore;
let currentAudioConstraint: boolean | MediaTrackConstraints = $audioConstraintStore;
let currentVideoConstraint: boolean|MediaTrackConstraints = $videoConstraintStore; if ($enableCameraSceneVisibilityStore) {
let currentAudioConstraint: boolean|MediaTrackConstraints = $audioConstraintStore;
if ($enableCameraSceneVisibilityStore) {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
return;
}
// Disable webcam if the user requested so
if ($requestedCameraState === false) {
currentVideoConstraint = false;
}
// Disable microphone if the user requested so
if ($requestedMicrophoneState === false) {
currentAudioConstraint = false;
}
// Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
}
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Let's make the changes only if the new value is different from the old one.
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
if (typeof previousComputedVideoConstraint !== 'boolean') {
previousComputedVideoConstraint = {...previousComputedVideoConstraint};
}
if (typeof previousComputedAudioConstraint !== 'boolean') {
previousComputedAudioConstraint = {...previousComputedAudioConstraint};
}
if (timeout) {
clearTimeout(timeout);
}
// Let's wait a little bit to avoid sending too many constraint changes.
timeout = setTimeout(() => {
set({ set({
video: currentVideoConstraint, video: currentVideoConstraint,
audio: currentAudioConstraint, audio: currentAudioConstraint,
}); });
}, 100); return;
} }
}, {
video: false, // Disable webcam if the user requested so
audio: false if ($requestedCameraState === false) {
} as MediaStreamConstraints); currentVideoConstraint = false;
}
// Disable microphone if the user requested so
if ($requestedMicrophoneState === false) {
currentAudioConstraint = false;
}
// Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
}
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Let's make the changes only if the new value is different from the old one.
if (
previousComputedVideoConstraint != currentVideoConstraint ||
previousComputedAudioConstraint != currentAudioConstraint
) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
if (typeof previousComputedVideoConstraint !== "boolean") {
previousComputedVideoConstraint = { ...previousComputedVideoConstraint };
}
if (typeof previousComputedAudioConstraint !== "boolean") {
previousComputedAudioConstraint = { ...previousComputedAudioConstraint };
}
if (timeout) {
clearTimeout(timeout);
}
// Let's wait a little bit to avoid sending too many constraint changes.
timeout = setTimeout(() => {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
}, 100);
}
},
{
video: false,
audio: false,
} as MediaStreamConstraints
);
export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue; export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
interface StreamSuccessValue { interface StreamSuccessValue {
type: "success", type: "success";
stream: MediaStream|null, stream: MediaStream | null;
// The constraints that we got (and not the one that have been requested) // The constraints that we got (and not the one that have been requested)
constraints: MediaStreamConstraints constraints: MediaStreamConstraints;
} }
interface StreamErrorValue { interface StreamErrorValue {
type: "error", type: "error";
error: Error, error: Error;
constraints: MediaStreamConstraints constraints: MediaStreamConstraints;
} }
let currentStream : MediaStream|null = null; let currentStream: MediaStream | null = null;
/** /**
* Stops the camera from filming * Stops the camera from filming
@ -347,84 +361,94 @@ function stopMicrophone(): void {
/** /**
* A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred) * A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred)
*/ */
export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(mediaStreamConstraintsStore, ($mediaStreamConstraintsStore, set) => { export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(
const constraints = { ...$mediaStreamConstraintsStore }; mediaStreamConstraintsStore,
($mediaStreamConstraintsStore, set) => {
const constraints = { ...$mediaStreamConstraintsStore };
if (navigator.mediaDevices === undefined) { if (navigator.mediaDevices === undefined) {
if (window.location.protocol === 'http:') { if (window.location.protocol === "http:") {
//throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'); //throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
set({
type: "error",
error: new Error("Unable to access your camera or microphone. You need to use a HTTPS connection."),
constraints,
});
return;
} else if (isIOS()) {
set({
type: "error",
error: new WebviewOnOldIOS(),
constraints,
});
return;
} else {
set({
type: "error",
error: new BrowserTooOldError(),
constraints,
});
return;
}
}
if (constraints.audio === false) {
stopMicrophone();
}
if (constraints.video === false) {
stopCamera();
}
if (constraints.audio === false && constraints.video === false) {
currentStream = null;
set({ set({
type: 'error', type: "success",
error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'), stream: null,
constraints constraints,
});
return;
} else if (isIOS()) {
set({
type: 'error',
error: new WebviewOnOldIOS(),
constraints
});
return;
} else {
set({
type: 'error',
error: new BrowserTooOldError(),
constraints
}); });
return; return;
} }
}
if (constraints.audio === false) { (async () => {
stopMicrophone(); try {
} stopMicrophone();
if (constraints.video === false) { stopCamera();
stopCamera(); currentStream = await navigator.mediaDevices.getUserMedia(constraints);
}
if (constraints.audio === false && constraints.video === false) {
currentStream = null;
set({
type: 'success',
stream: null,
constraints
});
return;
}
(async () => {
try {
stopMicrophone();
stopCamera();
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({
type: 'success',
stream: currentStream,
constraints
});
return;
} catch (e) {
if (constraints.video !== false) {
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
// TODO: does it make sense to pop this error when retrying?
set({ set({
type: 'error', type: "success",
error: e, stream: currentStream,
constraints constraints,
}); });
// Let's try without video constraints return;
requestedCameraState.disableWebcam(); } catch (e) {
} else { if (constraints.video !== false) {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e); console.info(
set({ "Error. Unable to get microphone and/or camera access. Trying audio only.",
type: 'error', $mediaStreamConstraintsStore,
error: e, e
constraints );
}); // TODO: does it make sense to pop this error when retrying?
} set({
type: "error",
error: e,
constraints,
});
// Let's try without video constraints
requestedCameraState.disableWebcam();
} else {
console.info(
"Error. Unable to get microphone and/or camera access.",
$mediaStreamConstraintsStore,
e
);
set({
type: "error",
error: e,
constraints,
});
}
/*constraints.video = false; /*constraints.video = false;
if (constraints.audio === false) { if (constraints.audio === false) {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e); console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
set({ set({
@ -453,9 +477,10 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
}); });
} }
}*/ }*/
} }
})(); })();
}); }
);
/** /**
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system) * A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
@ -472,12 +497,15 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
const queryDeviceList = () => { const queryDeviceList = () => {
// Note: so far, we are ignoring any failures. // Note: so far, we are ignoring any failures.
navigator.mediaDevices.enumerateDevices().then((mediaDeviceInfos) => { navigator.mediaDevices
set(mediaDeviceInfos); .enumerateDevices()
}).catch((e) => { .then((mediaDeviceInfos) => {
console.error(e); set(mediaDeviceInfos);
throw e; })
}); .catch((e) => {
console.error(e);
throw e;
});
}; };
const unsubscribe = localStreamStore.subscribe((streamResult) => { const unsubscribe = localStreamStore.subscribe((streamResult) => {
@ -490,23 +518,23 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
}); });
if (navigator.mediaDevices) { if (navigator.mediaDevices) {
navigator.mediaDevices.addEventListener('devicechange', queryDeviceList); navigator.mediaDevices.addEventListener("devicechange", queryDeviceList);
} }
return function stop() { return function stop() {
unsubscribe(); unsubscribe();
if (navigator.mediaDevices) { if (navigator.mediaDevices) {
navigator.mediaDevices.removeEventListener('devicechange', queryDeviceList); navigator.mediaDevices.removeEventListener("devicechange", queryDeviceList);
} }
}; };
}); });
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => { export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'videoinput'); return $deviceListStore.filter((device) => device.kind === "videoinput");
}); });
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => { export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'audioinput'); return $deviceListStore.filter((device) => device.kind === "audioinput");
}); });
// TODO: detect the new webcam and automatically switch on it. // TODO: detect the new webcam and automatically switch on it.
@ -519,7 +547,7 @@ cameraListStore.subscribe((devices) => {
// If we cannot find the device ID, let's remove it. // If we cannot find the device ID, let's remove it.
// @ts-ignore // @ts-ignore
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) { if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
videoConstraintStore.setDeviceId(undefined); videoConstraintStore.setDeviceId(undefined);
} }
}); });
@ -527,7 +555,7 @@ cameraListStore.subscribe((devices) => {
microphoneListStore.subscribe((devices) => { microphoneListStore.subscribe((devices) => {
// If the selected camera is unplugged, let's remove the constraint on deviceId // If the selected camera is unplugged, let's remove the constraint on deviceId
const constraints = get(audioConstraintStore); const constraints = get(audioConstraintStore);
if (typeof constraints === 'boolean') { if (typeof constraints === "boolean") {
return; return;
} }
if (!constraints.deviceId) { if (!constraints.deviceId) {
@ -536,13 +564,13 @@ microphoneListStore.subscribe((devices) => {
// If we cannot find the device ID, let's remove it. // If we cannot find the device ID, let's remove it.
// @ts-ignore // @ts-ignore
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) { if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
audioConstraintStore.setDeviceId(undefined); audioConstraintStore.setDeviceId(undefined);
} }
}); });
localStreamStore.subscribe(streamResult => { localStreamStore.subscribe((streamResult) => {
if (streamResult.type === 'error') { if (streamResult.type === "error") {
if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) { if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) {
errorStore.addErrorMessage(streamResult.error); errorStore.addErrorMessage(streamResult.error);
} }

View File

@ -1,7 +1,7 @@
import {readable, writable} from "svelte/store"; import { readable, writable } from "svelte/store";
import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import {VideoPeer} from "../WebRtc/VideoPeer"; import { VideoPeer } from "../WebRtc/VideoPeer";
import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
/** /**
* A store that contains the list of (video) peers we are connected to. * A store that contains the list of (video) peers we are connected to.
@ -19,20 +19,20 @@ function createPeerStore() {
simplePeer.registerPeerConnectionListener({ simplePeer.registerPeerConnectionListener({
onConnect(peer: RemotePeer) { onConnect(peer: RemotePeer) {
if (peer instanceof VideoPeer) { if (peer instanceof VideoPeer) {
update(users => { update((users) => {
users.set(peer.userId, peer); users.set(peer.userId, peer);
return users; return users;
}); });
} }
}, },
onDisconnect(userId: number) { onDisconnect(userId: number) {
update(users => { update((users) => {
users.delete(userId); users.delete(userId);
return users; return users;
}); });
} },
}) });
} },
}; };
} }
@ -52,20 +52,20 @@ function createScreenSharingPeerStore() {
simplePeer.registerPeerConnectionListener({ simplePeer.registerPeerConnectionListener({
onConnect(peer: RemotePeer) { onConnect(peer: RemotePeer) {
if (peer instanceof ScreenSharingPeer) { if (peer instanceof ScreenSharingPeer) {
update(users => { update((users) => {
users.set(peer.userId, peer); users.set(peer.userId, peer);
return users; return users;
}); });
} }
}, },
onDisconnect(userId: number) { onDisconnect(userId: number) {
update(users => { update((users) => {
users.delete(userId); users.delete(userId);
return users; return users;
}); });
} },
}) });
} },
}; };
} }
@ -79,8 +79,7 @@ function createScreenSharingStreamStore() {
let peers = new Map<number, ScreenSharingPeer>(); let peers = new Map<number, ScreenSharingPeer>();
return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) { return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) {
let unsubscribes: (() => void)[] = [];
let unsubscribes: (()=>void)[] = [];
const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => { const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => {
for (const unsubscribe of unsubscribes) { for (const unsubscribe of unsubscribes) {
@ -91,24 +90,23 @@ function createScreenSharingStreamStore() {
peers = new Map<number, ScreenSharingPeer>(); peers = new Map<number, ScreenSharingPeer>();
screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => { screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => {
if (screenSharingPeer.isReceivingScreenSharingStream()) { if (screenSharingPeer.isReceivingScreenSharingStream()) {
peers.set(key, screenSharingPeer); peers.set(key, screenSharingPeer);
} }
unsubscribes.push(screenSharingPeer.streamStore.subscribe((stream) => { unsubscribes.push(
if (stream) { screenSharingPeer.streamStore.subscribe((stream) => {
peers.set(key, screenSharingPeer); if (stream) {
} else { peers.set(key, screenSharingPeer);
peers.delete(key); } else {
} peers.delete(key);
set(peers); }
})); set(peers);
})
);
}); });
set(peers); set(peers);
}); });
return function stop() { return function stop() {
@ -117,9 +115,7 @@ function createScreenSharingStreamStore() {
unsubscribe(); unsubscribe();
} }
}; };
}) });
} }
export const screenSharingStreamStore = createScreenSharingStreamStore(); export const screenSharingStreamStore = createScreenSharingStreamStore();

View File

@ -0,0 +1,69 @@
import { writable } from "svelte/store";
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator";
let idCount = 0;
/**
* A store that contains the list of players currently known.
*/
function createPlayersStore() {
let players = new Map<number, PlayerInterface>();
const { subscribe, set, update } = writable(players);
return {
subscribe,
connectToRoomConnection: (roomConnection: RoomConnection) => {
players = new Map<number, PlayerInterface>();
set(players);
roomConnection.onUserJoins((message) => {
update((users) => {
users.set(message.userId, {
userId: message.userId,
name: message.name,
characterLayers: message.characterLayers,
visitCardUrl: message.visitCardUrl,
companion: message.companion,
userUuid: message.userUuid,
color: getRandomColor(),
});
return users;
});
});
roomConnection.onUserLeft((userId) => {
update((users) => {
users.delete(userId);
return users;
});
});
},
getPlayerById(userId: number): PlayerInterface | undefined {
return players.get(userId);
},
addFacticePlayer(name: string): number {
let userId: number | null = null;
players.forEach((p) => {
if (p.name === name) userId = p.userId;
});
if (userId) return userId;
const newUserId = idCount--;
update((users) => {
users.set(newUserId, {
userId: newUserId,
name,
characterLayers: [],
visitCardUrl: null,
companion: null,
userUuid: "dummy",
color: getRandomColor(),
});
return users;
});
return newUserId;
},
};
}
export const playersStore = createPlayersStore();

View File

@ -1,6 +1,6 @@
import {get, writable} from "svelte/store"; import { get, writable } from "svelte/store";
import {peerStore} from "./PeerStore"; import { peerStore } from "./PeerStore";
import {visibilityStore} from "./VisibilityStore"; import { visibilityStore } from "./VisibilityStore";
/** /**
* A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion. * A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion.
@ -28,7 +28,6 @@ function createPrivacyShutdownStore() {
} }
}); });
return { return {
subscribe, subscribe,
}; };

View File

@ -1,12 +1,10 @@
import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
import {peerStore} from "./PeerStore"; import { peerStore } from "./PeerStore";
import type { import type { LocalStreamStoreValue } from "./MediaStore";
LocalStreamStoreValue, import { DivImportance } from "../WebRtc/LayoutManager";
} from "./MediaStore"; import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility";
import {DivImportance} from "../WebRtc/LayoutManager";
import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility";
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
/** /**
* A store that contains the camera state requested by the user (on or off). * A store that contains the camera state requested by the user (on or off).
@ -23,7 +21,7 @@ function createRequestedScreenSharingState() {
export const requestedScreenSharingState = createRequestedScreenSharingState(); export const requestedScreenSharingState = createRequestedScreenSharingState();
let currentStream : MediaStream|null = null; let currentStream: MediaStream | null = null;
/** /**
* Stops the camera from filming * Stops the camera from filming
@ -37,27 +35,17 @@ function stopScreenSharing(): void {
currentStream = null; currentStream = null;
} }
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false; let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false; let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
/** /**
* A store containing the media constraints we want to apply. * A store containing the media constraints we want to apply.
*/ */
export const screenSharingConstraintsStore = derived( export const screenSharingConstraintsStore = derived(
[ [requestedScreenSharingState, gameOverlayVisibilityStore, peerStore],
requestedScreenSharingState, ([$requestedScreenSharingState, $gameOverlayVisibilityStore, $peerStore], set) => {
gameOverlayVisibilityStore, let currentVideoConstraint: boolean | MediaTrackConstraints = true;
peerStore, let currentAudioConstraint: boolean | MediaTrackConstraints = false;
], (
[
$requestedScreenSharingState,
$gameOverlayVisibilityStore,
$peerStore,
], set
) => {
let currentVideoConstraint: boolean|MediaTrackConstraints = true;
let currentAudioConstraint: boolean|MediaTrackConstraints = false;
// Disable screen sharing if the user requested so // Disable screen sharing if the user requested so
if (!$requestedScreenSharingState) { if (!$requestedScreenSharingState) {
@ -78,7 +66,10 @@ export const screenSharingConstraintsStore = derived(
} }
// Let's make the changes only if the new value is different from the old one. // Let's make the changes only if the new value is different from the old one.
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) { if (
previousComputedVideoConstraint != currentVideoConstraint ||
previousComputedAudioConstraint != currentAudioConstraint
) {
previousComputedVideoConstraint = currentVideoConstraint; previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint; previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects. // Let's copy the objects.
@ -94,85 +85,89 @@ export const screenSharingConstraintsStore = derived(
audio: currentAudioConstraint, audio: currentAudioConstraint,
}); });
} }
}, { },
{
video: false, video: false,
audio: false audio: false,
} as MediaStreamConstraints); } as MediaStreamConstraints
);
/** /**
* A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred) * A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred)
*/ */
export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(screenSharingConstraintsStore, ($screenSharingConstraintsStore, set) => { export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(
const constraints = $screenSharingConstraintsStore; screenSharingConstraintsStore,
($screenSharingConstraintsStore, set) => {
const constraints = $screenSharingConstraintsStore;
if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) { if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
set({
type: 'success',
stream: null,
constraints
});
return;
}
let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({constraints});
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({constraints});
} else {
stopScreenSharing();
set({
type: 'error',
error: new Error('Your browser does not support sharing screen'),
constraints
});
return;
}
(async () => {
try {
stopScreenSharing(); stopScreenSharing();
currentStream = await currentStreamPromise; requestedScreenSharingState.disableScreenSharing();
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of currentStream.getTracks()) {
track.onended = () => {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
previousComputedVideoConstraint = false;
previousComputedAudioConstraint = false;
set({
type: 'success',
stream: null,
constraints: {
video: false,
audio: false
}
});
};
}
set({ set({
type: 'success', type: "success",
stream: currentStream, stream: null,
constraints constraints,
}); });
return; return;
} catch (e) {
currentStream = null;
requestedScreenSharingState.disableScreenSharing();
console.info("Error. Unable to share screen.", e);
set({
type: 'error',
error: e,
constraints
});
} }
})();
}); let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({ constraints });
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({ constraints });
} else {
stopScreenSharing();
set({
type: "error",
error: new Error("Your browser does not support sharing screen"),
constraints,
});
return;
}
(async () => {
try {
stopScreenSharing();
currentStream = await currentStreamPromise;
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of currentStream.getTracks()) {
track.onended = () => {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
previousComputedVideoConstraint = false;
previousComputedAudioConstraint = false;
set({
type: "success",
stream: null,
constraints: {
video: false,
audio: false,
},
});
};
}
set({
type: "success",
stream: currentStream,
constraints,
});
return;
} catch (e) {
currentStream = null;
requestedScreenSharingState.disableScreenSharing();
console.info("Error. Unable to share screen.", e);
set({
type: "error",
error: e,
constraints,
});
}
})();
}
);
/** /**
* A store containing whether the screen sharing button should be displayed or hidden. * A store containing whether the screen sharing button should be displayed or hidden.
@ -188,19 +183,18 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set)
export interface ScreenSharingLocalMedia { export interface ScreenSharingLocalMedia {
uniqueId: string; uniqueId: string;
stream: MediaStream|null; stream: MediaStream | null;
//subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber; //subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber;
} }
/** /**
* The representation of the screen sharing stream. * The representation of the screen sharing stream.
*/ */
export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia|null>(null, function start(set) { export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia | null>(null, function start(set) {
const localMedia: ScreenSharingLocalMedia = { const localMedia: ScreenSharingLocalMedia = {
uniqueId: "localScreenSharingStream", uniqueId: "localScreenSharingStream",
stream: null stream: null,
} };
const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => { const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => {
if (screenSharingLocalStream.type === "success") { if (screenSharingLocalStream.type === "success") {
@ -214,4 +208,4 @@ export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia|null>(nu
return function stop() { return function stop() {
unsubscribe(); unsubscribe();
}; };
}) });

View File

@ -1,3 +1,3 @@
import {writable} from "svelte/store"; import { writable } from "svelte/store";
export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null); export const showReportScreenStore = writable<{ userId: number; userName: string } | null>(null);

View File

@ -1,8 +1,8 @@
import {derived, get, Readable, writable} from "svelte/store"; import { derived, get, Readable, writable } from "svelte/store";
import {ScreenSharingLocalMedia, screenSharingLocalMedia} from "./ScreenSharingStore"; import { ScreenSharingLocalMedia, screenSharingLocalMedia } from "./ScreenSharingStore";
import { peerStore, screenSharingStreamStore} from "./PeerStore"; import { peerStore, screenSharingStreamStore } from "./PeerStore";
import type {RemotePeer} from "../WebRtc/SimplePeer"; import type { RemotePeer } from "../WebRtc/SimplePeer";
import {LayoutMode} from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
export type Streamable = RemotePeer | ScreenSharingLocalMedia; export type Streamable = RemotePeer | ScreenSharingLocalMedia;
@ -12,32 +12,25 @@ export const layoutModeStore = writable<LayoutMode>(LayoutMode.Presentation);
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream) * A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
*/ */
function createStreamableCollectionStore(): Readable<Map<string, Streamable>> { function createStreamableCollectionStore(): Readable<Map<string, Streamable>> {
return derived(
[screenSharingStreamStore, peerStore, screenSharingLocalMedia],
([$screenSharingStreamStore, $peerStore, $screenSharingLocalMedia], set) => {
const peers = new Map<string, Streamable>();
return derived([ const addPeer = (peer: Streamable) => {
screenSharingStreamStore, peers.set(peer.uniqueId, peer);
peerStore, };
screenSharingLocalMedia,
], ([
$screenSharingStreamStore,
$peerStore,
$screenSharingLocalMedia,
], set) => {
const peers = new Map<string, Streamable>(); $screenSharingStreamStore.forEach(addPeer);
$peerStore.forEach(addPeer);
const addPeer = (peer: Streamable) => { if ($screenSharingLocalMedia?.stream) {
peers.set(peer.uniqueId, peer); addPeer($screenSharingLocalMedia);
}; }
$screenSharingStreamStore.forEach(addPeer); set(peers);
$peerStore.forEach(addPeer);
if ($screenSharingLocalMedia?.stream) {
addPeer($screenSharingLocalMedia);
} }
);
set(peers);
});
} }
export const streamableCollectionStore = createStreamableCollectionStore(); export const streamableCollectionStore = createStreamableCollectionStore();

View File

@ -1,10 +1,11 @@
import {derived} from "svelte/store"; import { derived } from "svelte/store";
import {consoleGlobalMessageManagerFocusStore} from "./ConsoleGlobalMessageManagerStore"; import { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore";
import { chatInputFocusStore } from "./ChatStore";
//derived from the focus on Menu, ConsoleGlobal, Chat and ... //derived from the focus on Menu, ConsoleGlobal, Chat and ...
export const enableUserInputsStore = derived( export const enableUserInputsStore = derived(
consoleGlobalMessageManagerFocusStore, [consoleGlobalMessageManagerFocusStore, chatInputFocusStore],
($consoleGlobalMessageManagerFocusStore) => { ([$consoleGlobalMessageManagerFocusStore, $chatInputFocusStore]) => {
return !$consoleGlobalMessageManagerFocusStore; return !$consoleGlobalMessageManagerFocusStore && !$chatInputFocusStore;
} }
); );

View File

@ -1,8 +1,8 @@
import {writable} from "svelte/store"; import { writable } from "svelte/store";
import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; import type { RemotePeer, SimplePeer } from "../WebRtc/SimplePeer";
import {VideoPeer} from "../WebRtc/VideoPeer"; import { VideoPeer } from "../WebRtc/VideoPeer";
import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; import { ScreenSharingPeer } from "../WebRtc/ScreenSharingPeer";
import type {Streamable} from "./StreamableCollectionStore"; import type { Streamable } from "./StreamableCollectionStore";
/** /**
* A store that contains the peer / media that has currently the "importance" focus. * A store that contains the peer / media that has currently the "importance" focus.
@ -32,15 +32,17 @@ function createVideoFocusStore() {
}, },
connectToSimplePeer: (simplePeer: SimplePeer) => { connectToSimplePeer: (simplePeer: SimplePeer) => {
simplePeer.registerPeerConnectionListener({ simplePeer.registerPeerConnectionListener({
onConnect(peer: RemotePeer) { onConnect(peer: RemotePeer) {},
},
onDisconnect(userId: number) { onDisconnect(userId: number) {
if ((focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) && focusedMedia.userId === userId) { if (
(focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) &&
focusedMedia.userId === userId
) {
set(null); set(null);
} }
} },
}) });
} },
}; };
} }

View File

@ -1,16 +1,16 @@
import {readable} from "svelte/store"; import { readable } from "svelte/store";
/** /**
* A store containing whether the current page is visible or not. * A store containing whether the current page is visible or not.
*/ */
export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) { export const visibilityStore = readable(document.visibilityState === "visible", function start(set) {
const onVisibilityChange = () => { const onVisibilityChange = () => {
set(document.visibilityState === 'visible'); set(document.visibilityState === "visible");
}; };
document.addEventListener('visibilitychange', onVisibilityChange); document.addEventListener("visibilitychange", onVisibilityChange);
return function stop() { return function stop() {
document.removeEventListener('visibilitychange', onVisibilityChange); document.removeEventListener("visibilitychange", onVisibilityChange);
}; };
}); });

View File

@ -1,23 +1,26 @@
import {Subject} from 'rxjs'; import { Subject } from "rxjs";
class BlackListManager { class BlackListManager {
private list: number[] = []; private list: string[] = [];
public onBlockStream: Subject<number> = new Subject(); public onBlockStream: Subject<string> = new Subject();
public onUnBlockStream: Subject<number> = new Subject(); public onUnBlockStream: Subject<string> = new Subject();
isBlackListed(userId: number): boolean { isBlackListed(userUuid: string): boolean {
return this.list.find((data) => data === userId) !== undefined; return this.list.find((data) => data === userUuid) !== undefined;
} }
blackList(userId: number): void { blackList(userUuid: string): void {
if (this.isBlackListed(userId)) return; if (this.isBlackListed(userUuid)) return;
this.list.push(userId); this.list.push(userUuid);
this.onBlockStream.next(userId); this.onBlockStream.next(userUuid);
} }
cancelBlackList(userId: number): void { cancelBlackList(userUuid: string): void {
this.list.splice(this.list.findIndex(data => data === userId), 1); this.list.splice(
this.onUnBlockStream.next(userId); this.list.findIndex((data) => data === userUuid),
1
);
this.onUnBlockStream.next(userUuid);
} }
} }

View File

@ -0,0 +1,52 @@
export function getRandomColor(): string {
const golden_ratio_conjugate = 0.618033988749895;
let hue = Math.random();
hue += golden_ratio_conjugate;
hue %= 1;
return hsv_to_rgb(hue, 0.5, 0.95);
}
//todo: test this.
function hsv_to_rgb(hue: number, saturation: number, brightness: number): string {
const h_i = Math.floor(hue * 6);
const f = hue * 6 - h_i;
const p = brightness * (1 - saturation);
const q = brightness * (1 - f * saturation);
const t = brightness * (1 - (1 - f) * saturation);
let r: number, g: number, b: number;
switch (h_i) {
case 0:
r = brightness;
g = t;
b = p;
break;
case 1:
r = q;
g = brightness;
b = p;
break;
case 2:
r = p;
g = brightness;
b = t;
break;
case 3:
r = p;
g = q;
b = brightness;
break;
case 4:
r = t;
g = p;
b = brightness;
break;
case 5:
r = brightness;
g = p;
b = q;
break;
default:
throw "h_i cannot be " + h_i;
}
return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
}

View File

@ -1,231 +1,13 @@
import {HtmlUtils} from "./HtmlUtils"; import { iframeListener } from "../Api/IframeListener";
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import { chatMessagesStore } from "../Stores/ChatStore";
import {connectionManager} from "../Connexion/ConnectionManager"; import { playersStore } from "../Stores/PlayersStore";
import {GameConnexionTypes} from "../Url/UrlManager";
import {iframeListener} from "../Api/IframeListener";
import {showReportScreenStore} from "../Stores/ShowReportScreenStore";
export type SendMessageCallback = (message:string) => void;
export class DiscussionManager { export class DiscussionManager {
private mainContainer: HTMLDivElement;
private divDiscuss?: HTMLDivElement;
private divParticipants?: HTMLDivElement;
private nbpParticipants?: HTMLParagraphElement;
private divMessages?: HTMLParagraphElement;
private participants: Map<number|string, HTMLDivElement> = new Map<number|string, HTMLDivElement>();
private activeDiscussion: boolean = false;
private sendMessageCallBack : Map<number|string, SendMessageCallback> = new Map<number|string, SendMessageCallback>();
private userInputManager?: UserInputManager;
constructor() { constructor() {
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
this.createDiscussPart(''); //todo: why do we always use empty string?
iframeListener.chatStream.subscribe((chatEvent) => { iframeListener.chatStream.subscribe((chatEvent) => {
this.addMessage(chatEvent.author, chatEvent.message, false); const userId = playersStore.addFacticePlayer(chatEvent.author);
this.showDiscussion(); chatMessagesStore.addExternalMessage(userId, chatEvent.message);
}); });
this.onSendMessageCallback('iframe_listener', (message) => {
iframeListener.sendUserInputChat(message);
})
}
private createDiscussPart(name: string) {
this.divDiscuss = document.createElement('div');
this.divDiscuss.classList.add('discussion');
const buttonCloseDiscussion: HTMLButtonElement = document.createElement('button');
buttonCloseDiscussion.classList.add('close-btn');
buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`;
buttonCloseDiscussion.addEventListener('click', () => {
this.hideDiscussion();
});
this.divDiscuss.appendChild(buttonCloseDiscussion);
const myName: HTMLParagraphElement = document.createElement('p');
myName.innerText = name.toUpperCase();
this.nbpParticipants = document.createElement('p');
this.nbpParticipants.innerText = 'PARTICIPANTS (1)';
this.divParticipants = document.createElement('div');
this.divParticipants.classList.add('participants');
this.divMessages = document.createElement('div');
this.divMessages.classList.add('messages');
this.divMessages.innerHTML = "<h2>Local messages</h2>"
this.divDiscuss.appendChild(myName);
this.divDiscuss.appendChild(this.nbpParticipants);
this.divDiscuss.appendChild(this.divParticipants);
this.divDiscuss.appendChild(this.divMessages);
const sendDivMessage: HTMLDivElement = document.createElement('div');
sendDivMessage.classList.add('send-message');
const inputMessage: HTMLInputElement = document.createElement('input');
inputMessage.onfocus = () => {
if(this.userInputManager) {
this.userInputManager.disableControls();
}
}
inputMessage.onblur = () => {
if(this.userInputManager) {
this.userInputManager.restoreControls();
}
}
inputMessage.type = "text";
inputMessage.addEventListener('keyup', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
if(inputMessage.value === null
|| inputMessage.value === ''
|| inputMessage.value === undefined) {
return;
}
this.addMessage(name, inputMessage.value, true);
for(const callback of this.sendMessageCallBack.values()) {
callback(inputMessage.value);
}
inputMessage.value = "";
}
});
sendDivMessage.appendChild(inputMessage);
this.divDiscuss.appendChild(sendDivMessage);
//append in main container
this.mainContainer.appendChild(this.divDiscuss);
this.addParticipant('me', 'Moi', undefined, true);
}
public addParticipant(
userId: number|'me',
name: string|undefined,
img?: string|undefined,
isMe: boolean = false,
) {
const divParticipant: HTMLDivElement = document.createElement('div');
divParticipant.classList.add('participant');
divParticipant.id = `participant-${userId}`;
const divImgParticipant: HTMLImageElement = document.createElement('img');
divImgParticipant.src = 'resources/logos/boy.svg';
if (img !== undefined) {
divImgParticipant.src = img;
}
const divPParticipant: HTMLParagraphElement = document.createElement('p');
if(!name){
name = 'Anonymous';
}
divPParticipant.innerText = name;
divParticipant.appendChild(divImgParticipant);
divParticipant.appendChild(divPParticipant);
if(
!isMe
&& connectionManager.getConnexionType
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous
&& userId !== 'me'
) {
const reportBanUserAction: HTMLButtonElement = document.createElement('button');
reportBanUserAction.classList.add('report-btn')
reportBanUserAction.innerText = 'Report';
reportBanUserAction.addEventListener('click', () => {
showReportScreenStore.set({ userId: userId, userName: name ? name : ''});
});
divParticipant.appendChild(reportBanUserAction);
}
this.divParticipants?.appendChild(divParticipant);
this.participants.set(userId, divParticipant);
this.updateParticipant(this.participants.size);
}
public updateParticipant(nb: number) {
if (!this.nbpParticipants) {
return;
}
this.nbpParticipants.innerText = `PARTICIPANTS (${nb})`;
}
public addMessage(name: string, message: string, isMe: boolean = false) {
const divMessage: HTMLDivElement = document.createElement('div');
divMessage.classList.add('message');
if(isMe){
divMessage.classList.add('me');
}
const pMessage: HTMLParagraphElement = document.createElement('p');
const date = new Date();
if(isMe){
name = 'Me';
} else {
name = HtmlUtils.escapeHtml(name);
}
pMessage.innerHTML = `<span style="font-weight: bold">${name}</span>
<span style="color:#bac2cc;display:inline-block;font-size:12px;">
${date.getHours()}:${date.getMinutes()}
</span>`;
divMessage.appendChild(pMessage);
const userMessage: HTMLParagraphElement = document.createElement('p');
userMessage.innerHTML = HtmlUtils.urlify(message);
userMessage.classList.add('body');
divMessage.appendChild(userMessage);
this.divMessages?.appendChild(divMessage);
//automatic scroll when there are new message
setTimeout(() => {
this.divMessages?.scroll({
top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y,
behavior: 'smooth'
});
}, 200);
}
public removeParticipant(userId: number|string){
const element = this.participants.get(userId);
if(element){
element.remove();
this.participants.delete(userId);
}
//if all participant leave, hide discussion button
this.sendMessageCallBack.delete(userId);
}
public onSendMessageCallback(userId: string|number, callback: SendMessageCallback): void {
this.sendMessageCallBack.set(userId, callback);
}
get activatedDiscussion(){
return this.activeDiscussion;
}
private showDiscussion(){
this.activeDiscussion = true;
this.divDiscuss?.classList.add('active');
}
private hideDiscussion(){
this.activeDiscussion = false;
this.divDiscuss?.classList.remove('active');
}
public setUserInputManager(userInputManager : UserInputManager){
this.userInputManager = userInputManager;
}
public showDiscussionPart(){
this.showDiscussion();
} }
} }

View File

@ -25,7 +25,7 @@ export class HtmlUtils {
} }
public static escapeHtml(html: string): string { public static escapeHtml(html: string): string {
const text = document.createTextNode(html); const text = document.createTextNode(html.replace(/(\r\n|\r|\n)/g,'<br/>'));
const p = document.createElement('p'); const p = document.createElement('p');
p.appendChild(text); p.appendChild(text);
return p.innerHTML; return p.innerHTML;

View File

@ -1,5 +1,5 @@
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import {HtmlUtils} from "./HtmlUtils"; import { HtmlUtils } from "./HtmlUtils";
export enum LayoutMode { export enum LayoutMode {
// All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle. // All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle.
@ -15,40 +15,40 @@ export enum DivImportance {
Normal = "Normal", Normal = "Normal",
} }
export const ON_ACTION_TRIGGER_BUTTON = 'onaction'; export const ON_ACTION_TRIGGER_BUTTON = "onaction";
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger'; export const TRIGGER_WEBSITE_PROPERTIES = "openWebsiteTrigger";
export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger'; export const TRIGGER_JITSI_PROPERTIES = "jitsiTrigger";
export const WEBSITE_MESSAGE_PROPERTIES = 'openWebsiteTriggerMessage'; export const WEBSITE_MESSAGE_PROPERTIES = "openWebsiteTriggerMessage";
export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage'; export const JITSI_MESSAGE_PROPERTIES = "jitsiTriggerMessage";
export const AUDIO_VOLUME_PROPERTY = 'audioVolume'; export const AUDIO_VOLUME_PROPERTY = "audioVolume";
export const AUDIO_LOOP_PROPERTY = 'audioLoop'; export const AUDIO_LOOP_PROPERTY = "audioLoop";
export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number}; export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };
class LayoutManager { class LayoutManager {
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>(); private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>(); private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){ public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager) {
//delete previous element //delete previous element
this.removeActionButton(id, userInputManager); this.removeActionButton(id, userInputManager);
//create div and text html component //create div and text html component
const p = document.createElement('p'); const p = document.createElement("p");
p.classList.add('action-body'); p.classList.add("action-body");
p.innerText = text; p.innerText = text;
const div = document.createElement('div'); const div = document.createElement("div");
div.classList.add('action'); div.classList.add("action");
div.id = id; div.id = id;
div.appendChild(p); div.appendChild(p);
this.actionButtonInformation.set(id, div); this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container'); const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
mainContainer.appendChild(div); mainContainer.appendChild(div);
//add trigger action //add trigger action
@ -57,42 +57,42 @@ class LayoutManager {
userInputManager.addSpaceEventListner(callBack); userInputManager.addSpaceEventListner(callBack);
} }
public removeActionButton(id: string, userInputManager?: UserInputManager){ public removeActionButton(id: string, userInputManager?: UserInputManager) {
//delete previous element //delete previous element
const previousDiv = this.actionButtonInformation.get(id); const previousDiv = this.actionButtonInformation.get(id);
if(previousDiv){ if (previousDiv) {
previousDiv.remove(); previousDiv.remove();
this.actionButtonInformation.delete(id); this.actionButtonInformation.delete(id);
} }
const previousEventCallback = this.actionButtonTrigger.get(id); const previousEventCallback = this.actionButtonTrigger.get(id);
if(previousEventCallback && userInputManager){ if (previousEventCallback && userInputManager) {
userInputManager.removeSpaceEventListner(previousEventCallback); userInputManager.removeSpaceEventListner(previousEventCallback);
} }
} }
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager){ public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager) {
//delete previous element //delete previous element
for ( const [key, value] of this.actionButtonInformation ) { for (const [key, value] of this.actionButtonInformation) {
this.removeActionButton(key, userInputManager); this.removeActionButton(key, userInputManager);
} }
//create div and text html component //create div and text html component
const p = document.createElement('p'); const p = document.createElement("p");
p.classList.add('action-body'); p.classList.add("action-body");
p.innerText = text; p.innerText = text;
const div = document.createElement('div'); const div = document.createElement("div");
div.classList.add('action'); div.classList.add("action");
div.classList.add(id); div.classList.add(id);
div.id = id; div.id = id;
div.appendChild(p); div.appendChild(p);
this.actionButtonInformation.set(id, div); this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container'); const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
mainContainer.appendChild(div); mainContainer.appendChild(div);
//add trigger action //add trigger action
if(callBack){ if (callBack) {
div.onpointerdown = () => { div.onpointerdown = () => {
callBack(); callBack();
this.removeActionButton(id, userInputManager); this.removeActionButton(id, userInputManager);
@ -102,7 +102,7 @@ class LayoutManager {
//remove it after 10 sec //remove it after 10 sec
setTimeout(() => { setTimeout(() => {
this.removeActionButton(id, userInputManager); this.removeActionButton(id, userInputManager);
}, 10000) }, 10000);
} }
} }

View File

@ -1,73 +1,65 @@
import { DivImportance, layoutManager } from "./LayoutManager"; import { layoutManager } from "./LayoutManager";
import { HtmlUtils } from "./HtmlUtils"; import { HtmlUtils } from "./HtmlUtils";
import { discussionManager, SendMessageCallback } from "./DiscussionManager";
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import { localUserStore } from "../Connexion/LocalUserStore"; import { localStreamStore } from "../Stores/MediaStore";
import type { UserSimplePeerInterface } from "./SimplePeer"; import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
import {
localStreamStore,
} from "../Stores/MediaStore";
import {
screenSharingLocalStreamStore
} from "../Stores/ScreenSharingStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void; export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void;
import {cowebsiteCloseButtonId} from "./CoWebsiteManager"; import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
export class MediaManager { export class MediaManager {
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>(); stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
private focused: boolean = true;
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>(); private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
constructor() { constructor() {
//Check of ask notification navigator permission
this.getNotification();
localStreamStore.subscribe((result) => { localStreamStore.subscribe((result) => {
if (result.type === 'error') { if (result.type === "error") {
console.error(result.error); console.error(result.error);
layoutManager.addInformation('warning', 'Camera access denied. Click here and check your browser permissions.', () => { layoutManager.addInformation(
helpCameraSettingsVisibleStore.set(true); "warning",
}, this.userInputManager); "Camera access denied. Click here and check your browser permissions.",
() => {
helpCameraSettingsVisibleStore.set(true);
},
this.userInputManager
);
return; return;
} }
}); });
screenSharingLocalStreamStore.subscribe((result) => { screenSharingLocalStreamStore.subscribe((result) => {
if (result.type === 'error') { if (result.type === "error") {
console.error(result.error); console.error(result.error);
layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check your browser permissions.', () => { layoutManager.addInformation(
helpCameraSettingsVisibleStore.set(true); "warning",
}, this.userInputManager); "Screen sharing denied. Click here and check your browser permissions.",
() => {
helpCameraSettingsVisibleStore.set(true);
},
this.userInputManager
);
return; return;
} }
}); });
} }
public showGameOverlay(): void { public showGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
gameOverlay.classList.add('active'); gameOverlay.classList.add("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId); const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => { const functionTrigger = () => {
this.triggerCloseJitsiFrameButton(); this.triggerCloseJitsiFrameButton();
} };
buttonCloseFrame.removeEventListener('click', () => { buttonCloseFrame.removeEventListener("click", () => {
buttonCloseFrame.blur(); buttonCloseFrame.blur();
functionTrigger(); functionTrigger();
}); });
@ -76,14 +68,14 @@ export class MediaManager {
} }
public hideGameOverlay(): void { public hideGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
gameOverlay.classList.remove('active'); gameOverlay.classList.remove("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId); const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => { const functionTrigger = () => {
this.triggerCloseJitsiFrameButton(); this.triggerCloseJitsiFrameButton();
} };
buttonCloseFrame.addEventListener('click', () => { buttonCloseFrame.addEventListener("click", () => {
buttonCloseFrame.blur(); buttonCloseFrame.blur();
functionTrigger(); functionTrigger();
}); });
@ -100,7 +92,7 @@ export class MediaManager {
if (!element) { if (!element) {
return; return;
} }
element.classList.add('active') //todo: why does a method 'disable' add a class 'active'? element.classList.add("active"); //todo: why does a method 'disable' add a class 'active'?
} }
enabledMicrophoneByUserId(userId: number) { enabledMicrophoneByUserId(userId: number) {
@ -108,7 +100,7 @@ export class MediaManager {
if (!element) { if (!element) {
return; return;
} }
element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'? element.classList.remove("active"); //todo: why does a method 'enable' remove a class 'active'?
} }
disabledVideoByUserId(userId: number) { disabledVideoByUserId(userId: number) {
@ -134,8 +126,8 @@ export class MediaManager {
} }
toggleBlockLogo(userId: number, show: boolean): void { toggleBlockLogo(userId: number, show: boolean): void {
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-' + userId); const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>("blocking-" + userId);
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active'); show ? blockLogoElement.classList.add("active") : blockLogoElement.classList.remove("active");
} }
isError(userId: string): void { isError(userId: string): void {
@ -144,27 +136,28 @@ export class MediaManager {
if (!element) { if (!element) {
return; return;
} }
const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement | null; const errorDiv = element.getElementsByClassName("rtc-error").item(0) as HTMLDivElement | null;
if (errorDiv === null) { if (errorDiv === null) {
return; return;
} }
errorDiv.style.display = 'block'; errorDiv.style.display = "block";
} }
isErrorScreenSharing(userId: string): void { isErrorScreenSharing(userId: string): void {
this.isError(this.getScreenSharingId(userId)); this.isError(this.getScreenSharingId(userId));
} }
private getSpinner(userId: string): HTMLDivElement | null { private getSpinner(userId: string): HTMLDivElement | null {
const element = document.getElementById(`div-${userId}`); const element = document.getElementById(`div-${userId}`);
if (!element) { if (!element) {
return null; return null;
} }
const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; const connectingSpinnerDiv = element
.getElementsByClassName("connecting-spinner")
.item(0) as HTMLDivElement | null;
return connectingSpinnerDiv; return connectingSpinnerDiv;
} }
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){ public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
this.triggerCloseJistiFrame.set(id, Function); this.triggerCloseJistiFrame.set(id, Function);
} }
@ -178,67 +171,35 @@ export class MediaManager {
} }
} }
public addNewMessage(name: string, message: string, isMe: boolean = false) { public setUserInputManager(userInputManager: UserInputManager) {
discussionManager.addMessage(name, message, isMe);
//when there are new message, show discussion
if (!discussionManager.activatedDiscussion) {
discussionManager.showDiscussionPart();
}
}
public addSendMessageCallback(userId: string | number, callback: SendMessageCallback) {
discussionManager.onSendMessageCallback(userId, callback);
}
public setUserInputManager(userInputManager : UserInputManager){
this.userInputManager = userInputManager; this.userInputManager = userInputManager;
discussionManager.setUserInputManager(userInputManager);
} }
public getNotification(){ public hasNotification(): boolean {
//Get notification return Notification.permission === "granted";
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") { }
if (this.checkNotificationPromise()) {
Notification.requestPermission().catch((err) => { public requestNotification() {
console.error(`Notification permission error`, err); if (window.Notification && Notification.permission !== "granted") {
}); return Notification.requestPermission();
} else { } else {
Notification.requestPermission(); return Promise.reject();
}
} }
} }
/** public createNotification(userName: string) {
* Return true if the browser supports the modern version of the Notification API (which is Promise based) or false if (document.hasFocus()) {
* if we are on Safari...
*
* See https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
*/
private checkNotificationPromise(): boolean {
try {
Notification.requestPermission().then();
} catch(e) {
return false;
}
return true;
}
public createNotification(userName: string){
if(this.focused){
return; return;
} }
if (window.Notification && Notification.permission === "granted") {
const title = 'WorkAdventure'; if (this.hasNotification()) {
const title = `${userName} wants to discuss with you`;
const options = { const options = {
body: `Hi! ${userName} wants to discuss with you, don't be afraid!`, icon: "/resources/logos/logo-WA-min.png",
icon: '/resources/logos/logo-WA-min.png', image: "/resources/logos/logo-WA-min.png",
image: '/resources/logos/logo-WA-min.png', badge: "/resources/logos/logo-WA-min.png",
badge: '/resources/logos/logo-WA-min.png',
}; };
new Notification(title, options); new Notification(title, options);
//new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);
} }
} }
} }

View File

@ -1,13 +1,12 @@
import type * as SimplePeerNamespace from "simple-peer"; import type * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager"; import type { RoomConnection } from "../Connexion/RoomConnection";
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable"; import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
import type {RoomConnection} from "../Connexion/RoomConnection"; import type { UserSimplePeerInterface } from "./SimplePeer";
import {MESSAGE_TYPE_CONSTRAINT, PeerStatus} from "./VideoPeer"; import { Readable, readable } from "svelte/store";
import type {UserSimplePeerInterface} from "./SimplePeer"; import { videoFocusStore } from "../Stores/VideoFocusStore";
import {Readable, readable, writable, Writable} from "svelte/store"; import { getIceServersConfig } from "../Components/Video/utils";
import {videoFocusStore} from "../Stores/VideoFocusStore";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
/** /**
* A peer connection used to transmit video / audio signals between 2 peers. * A peer connection used to transmit video / audio signals between 2 peers.
@ -16,7 +15,7 @@ export class ScreenSharingPeer extends Peer {
/** /**
* Whether this connection is currently receiving a video stream from a remote user. * Whether this connection is currently receiving a video stream from a remote user.
*/ */
private isReceivingStream:boolean = false; private isReceivingStream: boolean = false;
public toClose: boolean = false; public toClose: boolean = false;
public _connected: boolean = false; public _connected: boolean = false;
public readonly userId: number; public readonly userId: number;
@ -24,29 +23,25 @@ export class ScreenSharingPeer extends Peer {
public readonly streamStore: Readable<MediaStream | null>; public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>; public readonly statusStore: Readable<PeerStatus>;
constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) { constructor(
user: UserSimplePeerInterface,
initiator: boolean,
public readonly userName: string,
private connection: RoomConnection,
stream: MediaStream | null
) {
super({ super({
initiator: initiator ? initiator : false, initiator,
//reconnectTimer: 10000,
config: { config: {
iceServers: [ iceServers: getIceServersConfig(user),
{ },
urls: STUN_SERVER.split(',')
},
TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD
} : undefined,
].filter((value) => value !== undefined)
}
}); });
this.userId = user.userId; this.userId = user.userId;
this.uniqueId = 'screensharing_'+this.userId; this.uniqueId = "screensharing_" + this.userId;
this.streamStore = readable<MediaStream|null>(null, (set) => { this.streamStore = readable<MediaStream | null>(null, (set) => {
const onStream = (stream: MediaStream|null) => { const onStream = (stream: MediaStream | null) => {
videoFocusStore.focus(this); videoFocusStore.focus(this);
set(stream); set(stream);
}; };
@ -54,71 +49,71 @@ export class ScreenSharingPeer extends Peer {
// We unfortunately need to rely on an event to let the other party know a stream has stopped. // We unfortunately need to rely on an event to let the other party know a stream has stopped.
// It seems there is no native way to detect that. // It seems there is no native way to detect that.
// TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event // TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString("utf8"));
if (message.streamEnded !== true) { if (message.streamEnded !== true) {
console.error('Unexpected message on screen sharing peer connection'); console.error("Unexpected message on screen sharing peer connection");
return; return;
} }
set(null); set(null);
} };
this.on('stream', onStream); this.on("stream", onStream);
this.on('data', onData); this.on("data", onData);
return () => { return () => {
this.off('stream', onStream); this.off("stream", onStream);
this.off('data', onData); this.off("data", onData);
}; };
}); });
this.statusStore = readable<PeerStatus>("connecting", (set) => { this.statusStore = readable<PeerStatus>("connecting", (set) => {
const onConnect = () => { const onConnect = () => {
set('connected'); set("connected");
}; };
const onError = () => { const onError = () => {
set('error'); set("error");
}; };
const onClose = () => { const onClose = () => {
set('closed'); set("closed");
}; };
this.on('connect', onConnect); this.on("connect", onConnect);
this.on('error', onError); this.on("error", onError);
this.on('close', onClose); this.on("close", onClose);
return () => { return () => {
this.off('connect', onConnect); this.off("connect", onConnect);
this.off('error', onError); this.off("error", onError);
this.off('close', onClose); this.off("close", onClose);
}; };
}); });
//start listen signal for the peer connection //start listen signal for the peer connection
this.on('signal', (data: unknown) => { this.on("signal", (data: unknown) => {
this.sendWebrtcScreenSharingSignal(data); this.sendWebrtcScreenSharingSignal(data);
}); });
this.on('stream', (stream: MediaStream) => { this.on("stream", (stream: MediaStream) => {
this.stream(stream); this.stream(stream);
}); });
this.on('close', () => { this.on("close", () => {
this._connected = false; this._connected = false;
this.toClose = true; this.toClose = true;
this.destroy(); this.destroy();
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => { this.on("error", (err: any) => {
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err); console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
}); });
this.on('connect', () => { this.on("connect", () => {
this._connected = true; this._connected = true;
console.info(`connect => ${this.userId}`); console.info(`connect => ${this.userId}`);
}); });
this.once('finish', () => { this.once("finish", () => {
this._onFinish(); this._onFinish();
}); });
@ -130,7 +125,7 @@ export class ScreenSharingPeer extends Peer {
private sendWebrtcScreenSharingSignal(data: unknown) { private sendWebrtcScreenSharingSignal(data: unknown) {
try { try {
this.connection.sendWebrtcScreenSharingSignal(data, this.userId); this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
}catch (e) { } catch (e) {
console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e); console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e);
} }
} }
@ -139,7 +134,7 @@ export class ScreenSharingPeer extends Peer {
* Sends received stream to screen. * Sends received stream to screen.
*/ */
private stream(stream?: MediaStream) { private stream(stream?: MediaStream) {
if(!stream){ if (!stream) {
this.isReceivingStream = false; this.isReceivingStream = false;
} else { } else {
this.isReceivingStream = true; this.isReceivingStream = true;
@ -152,8 +147,8 @@ export class ScreenSharingPeer extends Peer {
public destroy(error?: Error): void { public destroy(error?: Error): void {
try { try {
this._connected = false this._connected = false;
if(!this.toClose){ if (!this.toClose) {
return; return;
} }
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
@ -162,24 +157,24 @@ export class ScreenSharingPeer extends Peer {
super.destroy(error); super.destroy(error);
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size); //console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) { } catch (err) {
console.error("ScreenSharingPeer::destroy", err) console.error("ScreenSharingPeer::destroy", err);
} }
} }
_onFinish () { _onFinish() {
if (this.destroyed) return if (this.destroyed) return;
const destroySoon = () => { const destroySoon = () => {
this.destroy(); this.destroy();
} };
if (this._connected) { if (this._connected) {
destroySoon(); destroySoon();
} else { } else {
this.once('connect', destroySoon); this.once("connect", destroySoon);
} }
} }
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) { public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream); this.removeStream(stream);
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true}))); this.write(new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true })));
} }
} }

View File

@ -2,26 +2,23 @@ import type {
WebRtcDisconnectMessageInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
} from "../Connexion/ConnexionModels"; } from "../Connexion/ConnexionModels";
import { import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback } from "./MediaManager";
mediaManager, import { ScreenSharingPeer } from "./ScreenSharingPeer";
StartScreenSharingCallback, import { MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer } from "./VideoPeer";
StopScreenSharingCallback, import type { RoomConnection } from "../Connexion/RoomConnection";
} from "./MediaManager"; import { blackListManager } from "./BlackListManager";
import {ScreenSharingPeer} from "./ScreenSharingPeer"; import { get } from "svelte/store";
import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore";
import type {RoomConnection} from "../Connexion/RoomConnection"; import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import {blackListManager} from "./BlackListManager"; import { discussionManager } from "./DiscussionManager";
import {get} from "svelte/store"; import { playersStore } from "../Stores/PlayersStore";
import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore"; import { newChatMessageStore } from "../Stores/ChatStore";
import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
import {discussionManager} from "./DiscussionManager";
export interface UserSimplePeerInterface{ export interface UserSimplePeerInterface {
userId: number; userId: number;
name?: string;
initiator?: boolean; initiator?: boolean;
webRtcUser?: string|undefined; webRtcUser?: string | undefined;
webRtcPassword?: string|undefined; webRtcPassword?: string | undefined;
} }
export type RemotePeer = VideoPeer | ScreenSharingPeer; export type RemotePeer = VideoPeer | ScreenSharingPeer;
@ -45,36 +42,40 @@ export class SimplePeer {
private readonly unsubscribers: (() => void)[] = []; private readonly unsubscribers: (() => void)[] = [];
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>(); private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number; private readonly userId: number;
private lastWebrtcUserName: string|undefined; private lastWebrtcUserName: string | undefined;
private lastWebrtcPassword: string|undefined; private lastWebrtcPassword: string | undefined;
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) { constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
// We need to go through this weird bound function pointer in order to be able to "free" this reference later. // We need to go through this weird bound function pointer in order to be able to "free" this reference later.
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this); this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
this.unsubscribers.push(localStreamStore.subscribe((streamResult) => { this.unsubscribers.push(
this.sendLocalVideoStream(streamResult); localStreamStore.subscribe((streamResult) => {
})); this.sendLocalVideoStream(streamResult);
})
);
let localScreenCapture: MediaStream|null = null; let localScreenCapture: MediaStream | null = null;
this.unsubscribers.push(screenSharingLocalStreamStore.subscribe((streamResult) => { this.unsubscribers.push(
if (streamResult.type === 'error') { screenSharingLocalStreamStore.subscribe((streamResult) => {
// Let's ignore screen sharing errors, we will deal with those in a different way. if (streamResult.type === "error") {
return; // Let's ignore screen sharing errors, we will deal with those in a different way.
} return;
if (streamResult.stream !== null) {
localScreenCapture = streamResult.stream;
this.sendLocalScreenSharingStream(localScreenCapture);
} else {
if (localScreenCapture) {
this.stopLocalScreenSharingStream(localScreenCapture);
localScreenCapture = null;
} }
}
})); if (streamResult.stream !== null) {
localScreenCapture = streamResult.stream;
this.sendLocalScreenSharingStream(localScreenCapture);
} else {
if (localScreenCapture) {
this.stopLocalScreenSharingStream(localScreenCapture);
localScreenCapture = null;
}
}
})
);
this.userId = Connection.getUserId(); this.userId = Connection.getUserId();
this.initialise(); this.initialise();
@ -92,7 +93,6 @@ export class SimplePeer {
* permit to listen when user could start visio * permit to listen when user could start visio
*/ */
private initialise() { private initialise() {
//receive signal by gemer //receive signal by gemer
this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => { this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
this.receiveWebrtcSignal(message); this.receiveWebrtcSignal(message);
@ -122,12 +122,12 @@ export class SimplePeer {
// This would be symmetrical to the way we handle disconnection. // This would be symmetrical to the way we handle disconnection.
//start connection //start connection
if(!user.initiator){ if (!user.initiator) {
return; return;
} }
const streamResult = get(localStreamStore); const streamResult = get(localStreamStore);
let stream : MediaStream | null = null; let stream: MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream) { if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream; stream = streamResult.stream;
} }
@ -137,15 +137,15 @@ export class SimplePeer {
/** /**
* create peer connection to bind users * create peer connection to bind users
*/ */
private createPeerConnection(user : UserSimplePeerInterface, localStream: MediaStream | null) : VideoPeer | null { private createPeerConnection(user: UserSimplePeerInterface, localStream: MediaStream | null): VideoPeer | null {
const peerConnection = this.PeerConnectionArray.get(user.userId) const peerConnection = this.PeerConnectionArray.get(user.userId);
if (peerConnection) { if (peerConnection) {
if (peerConnection.destroyed) { if (peerConnection.destroyed) {
peerConnection.toClose = true; peerConnection.toClose = true;
peerConnection.destroy(); peerConnection.destroy();
const peerConnexionDeleted = this.PeerConnectionArray.delete(user.userId); const peerConnexionDeleted = this.PeerConnectionArray.delete(user.userId);
if (!peerConnexionDeleted) { if (!peerConnexionDeleted) {
throw 'Error to delete peer connection'; throw "Error to delete peer connection";
} }
//return this.createPeerConnection(user, localStream); //return this.createPeerConnection(user, localStream);
} else { } else {
@ -154,36 +154,26 @@ export class SimplePeer {
} }
} }
let name = user.name; const name = this.getName(user.userId);
if (!name) {
name = this.getName(user.userId);
}
discussionManager.removeParticipant(user.userId);
this.lastWebrtcUserName = user.webRtcUser; this.lastWebrtcUserName = user.webRtcUser;
this.lastWebrtcPassword = user.webRtcPassword; this.lastWebrtcPassword = user.webRtcPassword;
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream); const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
//permit to send message
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), userId: this.userId, message: message})));
});
peer.toClose = false; peer.toClose = false;
// When a connection is established to a video stream, and if a screen sharing is taking place, // When a connection is established to a video stream, and if a screen sharing is taking place,
// the user sharing screen should also initiate a connection to the remote user! // the user sharing screen should also initiate a connection to the remote user!
peer.on('connect', () => { peer.on("connect", () => {
const streamResult = get(screenSharingLocalStreamStore); const streamResult = get(screenSharingLocalStreamStore);
if (streamResult.type === 'success' && streamResult.stream !== null) { if (streamResult.type === "success" && streamResult.stream !== null) {
this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream); this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream);
} }
}); });
//Create a notification for first user in circle discussion //Create a notification for first user in circle discussion
if(this.PeerConnectionArray.size === 0){ if (this.PeerConnectionArray.size === 0) {
mediaManager.createNotification(user.name??''); mediaManager.createNotification(name);
} }
this.PeerConnectionArray.set(user.userId, peer); this.PeerConnectionArray.set(user.userId, peer);
@ -194,29 +184,27 @@ export class SimplePeer {
} }
private getName(userId: number): string { private getName(userId: number): string {
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId); return playersStore.getPlayerById(userId)?.name || "";
if (userSearch) {
return userSearch.name || '';
} else {
return '';
}
} }
/** /**
* create peer connection to bind users * create peer connection to bind users
*/ */
private createPeerScreenSharingConnection(user : UserSimplePeerInterface, stream: MediaStream | null) : ScreenSharingPeer | null{ private createPeerScreenSharingConnection(
user: UserSimplePeerInterface,
stream: MediaStream | null
): ScreenSharingPeer | null {
const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId); const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId);
if(peerConnection){ if (peerConnection) {
if(peerConnection.destroyed){ if (peerConnection.destroyed) {
peerConnection.toClose = true; peerConnection.toClose = true;
peerConnection.destroy(); peerConnection.destroy();
const peerConnexionDeleted = this.PeerScreenSharingConnectionArray.delete(user.userId); const peerConnexionDeleted = this.PeerScreenSharingConnectionArray.delete(user.userId);
if(!peerConnexionDeleted){ if (!peerConnexionDeleted) {
throw 'Error to delete peer connection'; throw "Error to delete peer connection";
} }
this.createPeerConnection(user, stream); this.createPeerConnection(user, stream);
}else { } else {
peerConnection.toClose = false; peerConnection.toClose = false;
} }
return null; return null;
@ -230,7 +218,13 @@ export class SimplePeer {
const name = this.getName(user.userId); const name = this.getName(user.userId);
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream); const peer = new ScreenSharingPeer(
user,
user.initiator ? user.initiator : false,
name,
this.Connection,
stream
);
this.PeerScreenSharingConnectionArray.set(user.userId, peer); this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) { for (const peerConnectionListener of this.peerConnectionListeners) {
@ -242,11 +236,13 @@ export class SimplePeer {
/** /**
* This is triggered twice. Once by the server, and once by a remote client disconnecting * This is triggered twice. Once by the server, and once by a remote client disconnecting
*/ */
private closeConnection(userId : number) { private closeConnection(userId: number) {
try { try {
const peer = this.PeerConnectionArray.get(userId); const peer = this.PeerConnectionArray.get(userId);
if (peer === undefined) { if (peer === undefined) {
console.warn("closeConnection => Tried to close connection for user "+userId+" but could not find user"); console.warn(
"closeConnection => Tried to close connection for user " + userId + " but could not find user"
);
return; return;
} }
//create temp perr to close //create temp perr to close
@ -257,18 +253,18 @@ export class SimplePeer {
this.closeScreenSharingConnection(userId); this.closeScreenSharingConnection(userId);
const userIndex = this.Users.findIndex(user => user.userId === userId); const userIndex = this.Users.findIndex((user) => user.userId === userId);
if(userIndex < 0){ if (userIndex < 0) {
throw 'Couldn\'t delete user'; throw "Couldn't delete user";
} else { } else {
this.Users.splice(userIndex, 1); this.Users.splice(userIndex, 1);
} }
} catch (err) { } catch (err) {
console.error("closeConnection", err) console.error("closeConnection", err);
} }
//if user left discussion, clear array peer connection of sharing //if user left discussion, clear array peer connection of sharing
if(this.Users.length === 0) { if (this.Users.length === 0) {
for (const userId of this.PeerScreenSharingConnectionArray.keys()) { for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId); this.closeScreenSharingConnection(userId);
this.PeerScreenSharingConnectionArray.delete(userId); this.PeerScreenSharingConnectionArray.delete(userId);
@ -283,12 +279,16 @@ export class SimplePeer {
/** /**
* This is triggered twice. Once by the server, and once by a remote client disconnecting * This is triggered twice. Once by the server, and once by a remote client disconnecting
*/ */
private closeScreenSharingConnection(userId : number) { private closeScreenSharingConnection(userId: number) {
try { try {
//mediaManager.removeActiveScreenSharingVideo("" + userId); //mediaManager.removeActiveScreenSharingVideo("" + userId);
const peer = this.PeerScreenSharingConnectionArray.get(userId); const peer = this.PeerScreenSharingConnectionArray.get(userId);
if (peer === undefined) { if (peer === undefined) {
console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user") console.warn(
"closeScreenSharingConnection => Tried to close connection for user " +
userId +
" but could not find user"
);
return; return;
} }
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
@ -301,7 +301,7 @@ export class SimplePeer {
}*/ }*/
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size); //console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) { } catch (err) {
console.error("closeConnection", err) console.error("closeConnection", err);
} }
} }
@ -328,10 +328,10 @@ export class SimplePeer {
private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) { private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) {
try { try {
//if offer type, create peer connection //if offer type, create peer connection
if(data.signal.type === "offer"){ if (data.signal.type === "offer") {
const streamResult = get(localStreamStore); const streamResult = get(localStreamStore);
let stream : MediaStream | null = null; let stream: MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream) { if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream; stream = streamResult.stream;
} }
@ -341,7 +341,7 @@ export class SimplePeer {
if (peer !== undefined) { if (peer !== undefined) {
peer.signal(data.signal); peer.signal(data.signal);
} else { } else {
console.error('Could not find peer whose ID is "'+data.userId+'" in PeerConnectionArray'); console.error('Could not find peer whose ID is "' + data.userId + '" in PeerConnectionArray');
} }
} catch (e) { } catch (e) {
console.error(`receiveWebrtcSignal => ${data.userId}`, e); console.error(`receiveWebrtcSignal => ${data.userId}`, e);
@ -349,25 +349,28 @@ export class SimplePeer {
} }
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) { private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
if (blackListManager.isBlackListed(data.userId)) return; const uuid = playersStore.getPlayerById(data.userId)?.userUuid || "";
if (blackListManager.isBlackListed(uuid)) return;
console.log("receiveWebrtcScreenSharingSignal", data); console.log("receiveWebrtcScreenSharingSignal", data);
const streamResult = get(screenSharingLocalStreamStore); const streamResult = get(screenSharingLocalStreamStore);
let stream : MediaStream | null = null; let stream: MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream !== null) { if (streamResult.type === "success" && streamResult.stream !== null) {
stream = streamResult.stream; stream = streamResult.stream;
} }
try { try {
//if offer type, create peer connection //if offer type, create peer connection
if(data.signal.type === "offer"){ if (data.signal.type === "offer") {
this.createPeerScreenSharingConnection(data, stream); this.createPeerScreenSharingConnection(data, stream);
} }
const peer = this.PeerScreenSharingConnectionArray.get(data.userId); const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
if (peer !== undefined) { if (peer !== undefined) {
peer.signal(data.signal); peer.signal(data.signal);
} else { } else {
console.error('Could not find peer whose ID is "'+data.userId+'" in receiveWebrtcScreenSharingSignal'); console.error(
console.info('Attempt to create new peer connexion'); 'Could not find peer whose ID is "' + data.userId + '" in receiveWebrtcScreenSharingSignal'
);
console.info("Attempt to create new peer connexion");
if (stream) { if (stream) {
this.sendLocalScreenSharingStreamToUser(data.userId, stream); this.sendLocalScreenSharingStreamToUser(data.userId, stream);
} }
@ -384,17 +387,19 @@ export class SimplePeer {
try { try {
const PeerConnection = this.PeerConnectionArray.get(userId); const PeerConnection = this.PeerConnectionArray.get(userId);
if (!PeerConnection) { if (!PeerConnection) {
throw new Error('While adding media, cannot find user with ID ' + userId); throw new Error("While adding media, cannot find user with ID " + userId);
} }
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints}))); PeerConnection.write(
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints }))
);
if (streamResult.type === 'error') { if (streamResult.type === "error") {
return; return;
} }
const localStream: MediaStream | null = streamResult.stream; const localStream: MediaStream | null = streamResult.stream;
if(!localStream){ if (!localStream) {
return; return;
} }
@ -404,7 +409,7 @@ export class SimplePeer {
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any (track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
PeerConnection.addTrack(track, localStream); PeerConnection.addTrack(track, localStream);
} }
}catch (e) { } catch (e) {
console.error(`pushVideoToRemoteUser => ${userId}`, e); console.error(`pushVideoToRemoteUser => ${userId}`, e);
} }
} }
@ -412,7 +417,7 @@ export class SimplePeer {
private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) { private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) {
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId); const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnection) { if (!PeerConnection) {
throw new Error('While pushing screen sharing, cannot find user with ID ' + userId); throw new Error("While pushing screen sharing, cannot find user with ID " + userId);
} }
for (const track of localScreenCapture.getTracks()) { for (const track of localScreenCapture.getTracks()) {
@ -421,7 +426,7 @@ export class SimplePeer {
return; return;
} }
public sendLocalVideoStream(streamResult: LocalStreamStoreValue){ public sendLocalVideoStream(streamResult: LocalStreamStoreValue) {
for (const user of this.Users) { for (const user of this.Users) {
this.pushVideoToRemoteUser(user.userId, streamResult); this.pushVideoToRemoteUser(user.userId, streamResult);
} }
@ -446,7 +451,8 @@ export class SimplePeer {
} }
private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void { private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void {
if (blackListManager.isBlackListed(userId)) return; const uuid = playersStore.getPlayerById(userId)?.userUuid || "";
if (blackListManager.isBlackListed(uuid)) return;
// If a connection already exists with user (because it is already sharing a screen with us... let's use this connection) // If a connection already exists with user (because it is already sharing a screen with us... let's use this connection)
if (this.PeerScreenSharingConnectionArray.has(userId)) { if (this.PeerScreenSharingConnectionArray.has(userId)) {
this.pushScreenSharingToRemoteUser(userId, localScreenCapture); this.pushScreenSharingToRemoteUser(userId, localScreenCapture);
@ -455,9 +461,12 @@ export class SimplePeer {
const screenSharingUser: UserSimplePeerInterface = { const screenSharingUser: UserSimplePeerInterface = {
userId, userId,
initiator: true initiator: true,
}; };
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(screenSharingUser, localScreenCapture); const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(
screenSharingUser,
localScreenCapture
);
if (!PeerConnectionScreenSharing) { if (!PeerConnectionScreenSharing) {
return; return;
} }
@ -466,7 +475,7 @@ export class SimplePeer {
private stopLocalScreenSharingStreamToUser(userId: number, stream: MediaStream): void { private stopLocalScreenSharingStreamToUser(userId: number, stream: MediaStream): void {
const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId); const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnectionScreenSharing) { if (!PeerConnectionScreenSharing) {
throw new Error('Weird, screen sharing connection to user ' + userId + 'not found') throw new Error("Weird, screen sharing connection to user " + userId + "not found");
} }
console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing); console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing);

View File

@ -1,22 +1,23 @@
import type * as SimplePeerNamespace from "simple-peer"; import type * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager"; import { mediaManager } from "./MediaManager";
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable"; import type { RoomConnection } from "../Connexion/RoomConnection";
import type {RoomConnection} from "../Connexion/RoomConnection"; import { blackListManager } from "./BlackListManager";
import {blackListManager} from "./BlackListManager"; import type { Subscription } from "rxjs";
import type {Subscription} from "rxjs"; import type { UserSimplePeerInterface } from "./SimplePeer";
import type {UserSimplePeerInterface} from "./SimplePeer"; import { get, readable, Readable, Unsubscriber } from "svelte/store";
import {get, readable, Readable} from "svelte/store"; import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
import {obtainedMediaConstraintStore} from "../Stores/MediaStore"; import { playersStore } from "../Stores/PlayersStore";
import {discussionManager} from "./DiscussionManager"; import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore";
import { getIceServersConfig } from "../Components/Video/utils";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
export type PeerStatus = "connecting" | "connected" | "error" | "closed"; export type PeerStatus = "connecting" | "connected" | "error" | "closed";
export const MESSAGE_TYPE_CONSTRAINT = 'constraint'; export const MESSAGE_TYPE_CONSTRAINT = "constraint";
export const MESSAGE_TYPE_MESSAGE = 'message'; export const MESSAGE_TYPE_MESSAGE = "message";
export const MESSAGE_TYPE_BLOCKED = 'blocked'; export const MESSAGE_TYPE_BLOCKED = "blocked";
export const MESSAGE_TYPE_UNBLOCKED = 'unblocked'; export const MESSAGE_TYPE_UNBLOCKED = "unblocked";
/** /**
* A peer connection used to transmit video / audio signals between 2 peers. * A peer connection used to transmit video / audio signals between 2 peers.
*/ */
@ -26,121 +27,135 @@ export class VideoPeer extends Peer {
private remoteStream!: MediaStream; private remoteStream!: MediaStream;
private blocked: boolean = false; private blocked: boolean = false;
public readonly userId: number; public readonly userId: number;
public readonly userUuid: string;
public readonly uniqueId: string; public readonly uniqueId: string;
private onBlockSubscribe: Subscription; private onBlockSubscribe: Subscription;
private onUnBlockSubscribe: Subscription; private onUnBlockSubscribe: Subscription;
public readonly streamStore: Readable<MediaStream | null>; public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>; public readonly statusStore: Readable<PeerStatus>;
public readonly constraintsStore: Readable<MediaStreamConstraints|null>; public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
private newMessageunsubscriber: Unsubscriber | null = null;
private closing: Boolean = false; //this is used to prevent destroy() from being called twice
constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) { constructor(
public user: UserSimplePeerInterface,
initiator: boolean,
public readonly userName: string,
private connection: RoomConnection,
localStream: MediaStream | null
) {
super({ super({
initiator: initiator ? initiator : false, initiator,
//reconnectTimer: 10000,
config: { config: {
iceServers: [ iceServers: getIceServersConfig(user),
{ },
urls: STUN_SERVER.split(',')
},
TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD
} : undefined,
].filter((value) => value !== undefined)
}
}); });
this.userId = user.userId; this.userId = user.userId;
this.uniqueId = 'video_'+this.userId; this.userUuid = playersStore.getPlayerById(this.userId)?.userUuid || "";
this.uniqueId = "video_" + this.userId;
this.streamStore = readable<MediaStream|null>(null, (set) => { this.streamStore = readable<MediaStream | null>(null, (set) => {
const onStream = (stream: MediaStream|null) => { const onStream = (stream: MediaStream | null) => {
set(stream); set(stream);
}; };
const onData = (chunk: Buffer) => { const onData = (chunk: Buffer) => {
this.on('data', (chunk: Buffer) => { this.on("data", (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString("utf8"));
if (message.type === MESSAGE_TYPE_CONSTRAINT) { if (message.type === MESSAGE_TYPE_CONSTRAINT) {
if (!message.video) { if (!message.video) {
set(null); set(null);
} }
} }
}); });
} };
this.on('stream', onStream); this.on("stream", onStream);
this.on('data', onData); this.on("data", onData);
return () => { return () => {
this.off('stream', onStream); this.off("stream", onStream);
this.off('data', onData); this.off("data", onData);
}; };
}); });
this.constraintsStore = readable<MediaStreamConstraints|null>(null, (set) => { this.constraintsStore = readable<MediaStreamConstraints | null>(null, (set) => {
const onData = (chunk: Buffer) => { const onData = (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString("utf8"));
if(message.type === MESSAGE_TYPE_CONSTRAINT) { if (message.type === MESSAGE_TYPE_CONSTRAINT) {
set(message); set(message);
} }
} };
this.on('data', onData); this.on("data", onData);
return () => { return () => {
this.off('data', onData); this.off("data", onData);
}; };
}); });
this.statusStore = readable<PeerStatus>("connecting", (set) => { this.statusStore = readable<PeerStatus>("connecting", (set) => {
const onConnect = () => { const onConnect = () => {
set('connected'); set("connected");
}; };
const onError = () => { const onError = () => {
set('error'); set("error");
}; };
const onClose = () => { const onClose = () => {
set('closed'); set("closed");
}; };
this.on('connect', onConnect); this.on("connect", onConnect);
this.on('error', onError); this.on("error", onError);
this.on('close', onClose); this.on("close", onClose);
return () => { return () => {
this.off('connect', onConnect); this.off("connect", onConnect);
this.off('error', onError); this.off("error", onError);
this.off('close', onClose); this.off("close", onClose);
}; };
}); });
//start listen signal for the peer connection //start listen signal for the peer connection
this.on('signal', (data: unknown) => { this.on("signal", (data: unknown) => {
this.sendWebrtcSignal(data); this.sendWebrtcSignal(data);
}); });
this.on('stream', (stream: MediaStream) => this.stream(stream)); this.on("stream", (stream: MediaStream) => this.stream(stream));
this.on('close', () => { this.on("close", () => {
this._connected = false; this._connected = false;
this.toClose = true; this.toClose = true;
this.destroy(); this.destroy();
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => { this.on("error", (err: any) => {
console.error(`error => ${this.userId} => ${err.code}`, err); console.error(`error => ${this.userId} => ${err.code}`, err);
mediaManager.isError("" + this.userId); mediaManager.isError("" + this.userId);
}); });
this.on('connect', () => { this.on("connect", () => {
this._connected = true; this._connected = true;
chatMessagesStore.addIncomingUser(this.userId);
this.newMessageunsubscriber = newChatMessageStore.subscribe((newMessage) => {
if (!newMessage) return;
this.write(
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_MESSAGE,
message: newMessage,
})
)
); //send more data
newChatMessageStore.set(null); //This is to prevent a newly created SimplePeer to send an old message a 2nd time. Is there a better way?
});
}); });
this.on('data', (chunk: Buffer) => { this.on("data", (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString("utf8"));
if(message.type === MESSAGE_TYPE_CONSTRAINT) { if (message.type === MESSAGE_TYPE_CONSTRAINT) {
if (message.audio) { if (message.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId); mediaManager.enabledMicrophoneByUserId(this.userId);
} else { } else {
@ -152,58 +167,67 @@ export class VideoPeer extends Peer {
} else { } else {
mediaManager.disabledVideoByUserId(this.userId); mediaManager.disabledVideoByUserId(this.userId);
} }
} else if(message.type === MESSAGE_TYPE_MESSAGE) { } else if (message.type === MESSAGE_TYPE_MESSAGE) {
if (!blackListManager.isBlackListed(message.userId)) { if (!blackListManager.isBlackListed(this.userUuid)) {
mediaManager.addNewMessage(message.name, message.message); chatMessagesStore.addExternalMessage(this.userId, message.message);
} }
} else if(message.type === MESSAGE_TYPE_BLOCKED) { } else if (message.type === MESSAGE_TYPE_BLOCKED) {
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream. //FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
// Find a way to block A's output stream in A's js client // Find a way to block A's output stream in A's js client
//However, the output stream stream B is correctly blocked in A client //However, the output stream stream B is correctly blocked in A client
this.blocked = true; this.blocked = true;
this.toggleRemoteStream(false); this.toggleRemoteStream(false);
} else if(message.type === MESSAGE_TYPE_UNBLOCKED) { } else if (message.type === MESSAGE_TYPE_UNBLOCKED) {
this.blocked = false; this.blocked = false;
this.toggleRemoteStream(true); this.toggleRemoteStream(true);
} }
}); });
this.once('finish', () => { this.once("finish", () => {
this._onFinish(); this._onFinish();
}); });
this.pushVideoToRemoteUser(localStream); this.pushVideoToRemoteUser(localStream);
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userId) => { this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userUuid) => {
if (userId === this.userId) { if (userUuid === this.userUuid) {
this.toggleRemoteStream(false); this.toggleRemoteStream(false);
this.sendBlockMessage(true); this.sendBlockMessage(true);
} }
}); });
this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userId) => { this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userUuid) => {
if (userId === this.userId) { if (userUuid === this.userUuid) {
this.toggleRemoteStream(true); this.toggleRemoteStream(true);
this.sendBlockMessage(false); this.sendBlockMessage(false);
} }
}); });
if (blackListManager.isBlackListed(this.userId)) { if (blackListManager.isBlackListed(this.userUuid)) {
this.sendBlockMessage(true) this.sendBlockMessage(true);
} }
} }
private sendBlockMessage(blocking: boolean) { private sendBlockMessage(blocking: boolean) {
this.write(new Buffer(JSON.stringify({type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED, name: this.userName.toUpperCase(), userId: this.userId, message: ''}))); this.write(
new Buffer(
JSON.stringify({
type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED,
name: this.userName.toUpperCase(),
userId: this.userId,
message: "",
})
)
);
} }
private toggleRemoteStream(enable: boolean) { private toggleRemoteStream(enable: boolean) {
this.remoteStream.getTracks().forEach(track => track.enabled = enable); this.remoteStream.getTracks().forEach((track) => (track.enabled = enable));
mediaManager.toggleBlockLogo(this.userId, !enable); mediaManager.toggleBlockLogo(this.userId, !enable);
} }
private sendWebrtcSignal(data: unknown) { private sendWebrtcSignal(data: unknown) {
try { try {
this.connection.sendWebrtcSignal(data, this.userId); this.connection.sendWebrtcSignal(data, this.userId);
}catch (e) { } catch (e) {
console.error(`sendWebrtcSignal => ${this.userId}`, e); console.error(`sendWebrtcSignal => ${this.userId}`, e);
} }
} }
@ -214,10 +238,10 @@ export class VideoPeer extends Peer {
private stream(stream: MediaStream) { private stream(stream: MediaStream) {
try { try {
this.remoteStream = stream; this.remoteStream = stream;
if (blackListManager.isBlackListed(this.userId) || this.blocked) { if (blackListManager.isBlackListed(this.userUuid) || this.blocked) {
this.toggleRemoteStream(false); this.toggleRemoteStream(false);
} }
}catch (err){ } catch (err) {
console.error(err); console.error(err);
} }
} }
@ -225,47 +249,49 @@ export class VideoPeer extends Peer {
/** /**
* This is triggered twice. Once by the server, and once by a remote client disconnecting * This is triggered twice. Once by the server, and once by a remote client disconnecting
*/ */
public destroy(error?: Error): void { public destroy(): void {
try { try {
this._connected = false this._connected = false;
if(!this.toClose){ if (!this.toClose || this.closing) {
return; return;
} }
this.closing = true;
this.onBlockSubscribe.unsubscribe(); this.onBlockSubscribe.unsubscribe();
this.onUnBlockSubscribe.unsubscribe(); this.onUnBlockSubscribe.unsubscribe();
discussionManager.removeParticipant(this.userId); if (this.newMessageunsubscriber) this.newMessageunsubscriber();
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" chatMessagesStore.addOutcomingUser(this.userId);
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. super.destroy();
super.destroy(error);
} catch (err) { } catch (err) {
console.error("VideoPeer::destroy", err) console.error("VideoPeer::destroy", err);
} }
} }
_onFinish () { _onFinish() {
if (this.destroyed) return if (this.destroyed) return;
const destroySoon = () => { const destroySoon = () => {
this.destroy(); this.destroy();
} };
if (this._connected) { if (this._connected) {
destroySoon(); destroySoon();
} else { } else {
this.once('connect', destroySoon); this.once("connect", destroySoon);
} }
} }
private pushVideoToRemoteUser(localStream: MediaStream | null) { private pushVideoToRemoteUser(localStream: MediaStream | null) {
try { try {
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore)}))); this.write(
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore) }))
);
if(!localStream){ if (!localStream) {
return; return;
} }
for (const track of localStream.getTracks()) { for (const track of localStream.getTracks()) {
this.addTrack(track, localStream); this.addTrack(track, localStream);
} }
}catch (e) { } catch (e) {
console.error(`pushVideoToRemoteUser => ${this.userId}`, e); console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
} }
} }

View File

@ -1,12 +1,12 @@
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks"; import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
import { import {
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap, isIframeAnswerEvent, isIframeErrorAnswerEvent,
isIframeResponseEventWrapper, isIframeResponseEventWrapper,
TypedMessageEvent TypedMessageEvent,
} from "./Api/Events/IframeEvent"; } from "./Api/Events/IframeEvent";
import chat from "./Api/iframe/chat"; import chat from "./Api/iframe/chat";
import type { IframeCallback } from './Api/iframe/IframeApiContribution'; import type { IframeCallback } from "./Api/iframe/IframeApiContribution";
import nav from "./Api/iframe/nav"; import nav from "./Api/iframe/nav";
import controls from "./Api/iframe/controls"; import controls from "./Api/iframe/controls";
import ui from "./Api/iframe/ui"; import ui from "./Api/iframe/ui";
@ -16,7 +16,7 @@ import player from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound"; import type { Sound } from "./Api/iframe/Sound/Sound";
import {sendToWorkadventure} from "./Api/iframe/IframeApiContribution"; import { answerPromises, sendToWorkadventure} from "./Api/iframe/IframeApiContribution";
const wa = { const wa = {
ui, ui,
@ -34,7 +34,7 @@ const wa = {
* @deprecated Use WA.chat.sendChatMessage instead * @deprecated Use WA.chat.sendChatMessage instead
*/ */
sendChatMessage(message: string, author: string): void { sendChatMessage(message: string, author: string): void {
console.warn('Method WA.sendChatMessage is deprecated. Please use WA.chat.sendChatMessage instead'); console.warn("Method WA.sendChatMessage is deprecated. Please use WA.chat.sendChatMessage instead");
chat.sendChatMessage(message, author); chat.sendChatMessage(message, author);
}, },
@ -42,7 +42,9 @@ const wa = {
* @deprecated Use WA.chat.disablePlayerControls instead * @deprecated Use WA.chat.disablePlayerControls instead
*/ */
disablePlayerControls(): void { disablePlayerControls(): void {
console.warn('Method WA.disablePlayerControls is deprecated. Please use WA.controls.disablePlayerControls instead'); console.warn(
"Method WA.disablePlayerControls is deprecated. Please use WA.controls.disablePlayerControls instead"
);
controls.disablePlayerControls(); controls.disablePlayerControls();
}, },
@ -50,7 +52,9 @@ const wa = {
* @deprecated Use WA.controls.restorePlayerControls instead * @deprecated Use WA.controls.restorePlayerControls instead
*/ */
restorePlayerControls(): void { restorePlayerControls(): void {
console.warn('Method WA.restorePlayerControls is deprecated. Please use WA.controls.restorePlayerControls instead'); console.warn(
"Method WA.restorePlayerControls is deprecated. Please use WA.controls.restorePlayerControls instead"
);
controls.restorePlayerControls(); controls.restorePlayerControls();
}, },
@ -58,7 +62,7 @@ const wa = {
* @deprecated Use WA.ui.displayBubble instead * @deprecated Use WA.ui.displayBubble instead
*/ */
displayBubble(): void { displayBubble(): void {
console.warn('Method WA.displayBubble is deprecated. Please use WA.ui.displayBubble instead'); console.warn("Method WA.displayBubble is deprecated. Please use WA.ui.displayBubble instead");
ui.displayBubble(); ui.displayBubble();
}, },
@ -66,7 +70,7 @@ const wa = {
* @deprecated Use WA.ui.removeBubble instead * @deprecated Use WA.ui.removeBubble instead
*/ */
removeBubble(): void { removeBubble(): void {
console.warn('Method WA.removeBubble is deprecated. Please use WA.ui.removeBubble instead'); console.warn("Method WA.removeBubble is deprecated. Please use WA.ui.removeBubble instead");
ui.removeBubble(); ui.removeBubble();
}, },
@ -74,7 +78,7 @@ const wa = {
* @deprecated Use WA.nav.openTab instead * @deprecated Use WA.nav.openTab instead
*/ */
openTab(url: string): void { openTab(url: string): void {
console.warn('Method WA.openTab is deprecated. Please use WA.nav.openTab instead'); console.warn("Method WA.openTab is deprecated. Please use WA.nav.openTab instead");
nav.openTab(url); nav.openTab(url);
}, },
@ -82,7 +86,7 @@ const wa = {
* @deprecated Use WA.sound.loadSound instead * @deprecated Use WA.sound.loadSound instead
*/ */
loadSound(url: string): Sound { loadSound(url: string): Sound {
console.warn('Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead'); console.warn("Method WA.loadSound is deprecated. Please use WA.sound.loadSound instead");
return sound.loadSound(url); return sound.loadSound(url);
}, },
@ -90,7 +94,7 @@ const wa = {
* @deprecated Use WA.nav.goToPage instead * @deprecated Use WA.nav.goToPage instead
*/ */
goToPage(url: string): void { goToPage(url: string): void {
console.warn('Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead'); console.warn("Method WA.goToPage is deprecated. Please use WA.nav.goToPage instead");
nav.goToPage(url); nav.goToPage(url);
}, },
@ -98,7 +102,7 @@ const wa = {
* @deprecated Use WA.nav.goToRoom instead * @deprecated Use WA.nav.goToRoom instead
*/ */
goToRoom(url: string): void { goToRoom(url: string): void {
console.warn('Method WA.goToRoom is deprecated. Please use WA.nav.goToRoom instead'); console.warn("Method WA.goToRoom is deprecated. Please use WA.nav.goToRoom instead");
nav.goToRoom(url); nav.goToRoom(url);
}, },
@ -106,7 +110,7 @@ const wa = {
* @deprecated Use WA.nav.openCoWebSite instead * @deprecated Use WA.nav.openCoWebSite instead
*/ */
openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void { openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void {
console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead'); console.warn("Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead");
nav.openCoWebSite(url, allowApi, allowPolicy); nav.openCoWebSite(url, allowApi, allowPolicy);
}, },
@ -114,36 +118,36 @@ const wa = {
* @deprecated Use WA.nav.closeCoWebSite instead * @deprecated Use WA.nav.closeCoWebSite instead
*/ */
closeCoWebSite(): void { closeCoWebSite(): void {
console.warn('Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead'); console.warn("Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead");
nav.closeCoWebSite(); nav.closeCoWebSite();
}, },
/** /**
* @deprecated Use WA.controls.restorePlayerControls instead * @deprecated Use WA.ui.openPopup instead
*/ */
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
console.warn('Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead'); console.warn("Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead");
return ui.openPopup(targetObject, message, buttons); return ui.openPopup(targetObject, message, buttons);
}, },
/** /**
* @deprecated Use WA.chat.onChatMessage instead * @deprecated Use WA.chat.onChatMessage instead
*/ */
onChatMessage(callback: (message: string) => void): void { onChatMessage(callback: (message: string) => void): void {
console.warn('Method WA.onChatMessage is deprecated. Please use WA.chat.onChatMessage instead'); console.warn("Method WA.onChatMessage is deprecated. Please use WA.chat.onChatMessage instead");
chat.onChatMessage(callback); chat.onChatMessage(callback);
}, },
/** /**
* @deprecated Use WA.room.onEnterZone instead * @deprecated Use WA.room.onEnterZone instead
*/ */
onEnterZone(name: string, callback: () => void): void { onEnterZone(name: string, callback: () => void): void {
console.warn('Method WA.onEnterZone is deprecated. Please use WA.room.onEnterZone instead'); console.warn("Method WA.onEnterZone is deprecated. Please use WA.room.onEnterZone instead");
room.onEnterZone(name, callback); room.onEnterZone(name, callback);
}, },
/** /**
* @deprecated Use WA.room.onLeaveZone instead * @deprecated Use WA.room.onLeaveZone instead
*/ */
onLeaveZone(name: string, callback: () => void): void { onLeaveZone(name: string, callback: () => void): void {
console.warn('Method WA.onLeaveZone is deprecated. Please use WA.room.onLeaveZone instead'); console.warn("Method WA.onLeaveZone is deprecated. Please use WA.room.onLeaveZone instead");
room.onLeaveZone(name, callback); room.onLeaveZone(name, callback);
}, },
}; };
@ -151,30 +155,54 @@ const wa = {
export type WorkAdventureApi = typeof wa; export type WorkAdventureApi = typeof wa;
declare global { declare global {
interface Window { interface Window {
WA: WorkAdventureApi WA: WorkAdventureApi;
} }
let WA: WorkAdventureApi let WA: WorkAdventureApi;
} }
window.WA = wa; window.WA = wa;
window.addEventListener('message', <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => { window.addEventListener(
"message", <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
if (message.source !== window.parent) { if (message.source !== window.parent) {
return; // Skip message in this event listener return; // Skip message in this event listener
} }
const payload = message.data; const payload = message.data;
console.debug(payload); console.debug(payload);
if (isIframeResponseEventWrapper(payload)) { if (isIframeAnswerEvent(payload)) {
const queryId = payload.id;
const payloadData = payload.data; const payloadData = payload.data;
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined const resolver = answerPromises.get(queryId);
if (callback?.typeChecker(payloadData)) { if (resolver === undefined) {
callback?.callback(payloadData) throw new Error('In Iframe API, got an answer for a question that we have no track of.');
} }
} resolver.resolve(payloadData);
// ... answerPromises.delete(queryId);
}); } else if (isIframeErrorAnswerEvent(payload)) {
const queryId = payload.id;
const payloadError = payload.error;
const resolver = answerPromises.get(queryId);
if (resolver === undefined) {
throw new Error('In Iframe API, got an error answer for a question that we have no track of.');
}
resolver.reject(payloadError);
answerPromises.delete(queryId);
} else if (isIframeResponseEventWrapper(payload)) {
const payloadData = payload.data;
const callback = registeredCallbacks[payload.type] as IframeCallback<T> | undefined;
if (callback?.typeChecker(payloadData)) {
callback?.callback(payloadData);
}
}
// ...
}
);

View File

@ -160,3 +160,15 @@ const app = new App({
}) })
export default app export default app
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/resources/service-worker.js')
.then(serviceWorker => {
console.log("Service Worker registered: ", serviceWorker);
})
.catch(error => {
console.error("Error registering the Service Worker: ", error);
});
});
}

View File

@ -1,9 +1,5 @@
@import "~@fontsource/press-start-2p/index.css"; @import "~@fontsource/press-start-2p/index.css";
*{
font-family: PixelFont-7,monospace;
}
.nes-btn { .nes-btn {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
} }

View File

@ -1,5 +1,5 @@
*{ *{
font-family: 'Open Sans', sans-serif; font-family: Lato;
cursor: url('./images/cursor_normal.png'), auto; cursor: url('./images/cursor_normal.png'), auto;
} }
* a, button, select{ * a, button, select{

View File

@ -1,58 +0,0 @@
import "jasmine";
import {Room} from "../../../src/Connexion/Room";
describe("Room getIdFromIdentifier()", () => {
it("should work with an absolute room id and no hash as parameter", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', '', '');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with an absolute room id and a hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json#start', '', '');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual("start");
});
it("should work with an absolute room id, regardless of baseUrl or instance", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', 'https://another.domain/_/global/test.json', 'lol');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link and no hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('./test2.json', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link with no dot", () => {
const {roomId, hash} = Room.getIdFromIdentifier('test2.json', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link two levels deep", () => {
const {roomId, hash} = Room.getIdFromIdentifier('../floor1/Floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/floor1/Floor1.json');
expect(hash).toEqual('');
});
it("should work with a relative file link that rewrite the map domain", () => {
const {roomId, hash} = Room.getIdFromIdentifier('../../maps.workadventure.localhost/Floor1/floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventure.localhost/Floor1/floor1.json');
expect(hash).toEqual('');
});
it("should work with a relative file link that rewrite the map instance", () => {
const {roomId, hash} = Room.getIdFromIdentifier('../../../notglobal/maps.workadventu.re/Floor1/floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
expect(roomId).toEqual('_/notglobal/maps.workadventu.re/Floor1/floor1.json');
expect(hash).toEqual('');
});
it("should work with a relative file link that change the map type", () => {
const {roomId, hash} = Room.getIdFromIdentifier('../../../../@/tcm/is/great', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
expect(roomId).toEqual('@/tcm/is/great');
expect(hash).toEqual('');
});
it("should work with a relative file link and a hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('./test2.json#start', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual("start");
});
});

View File

@ -1,150 +1,155 @@
import "jasmine"; import "jasmine";
import {Room} from "../../../src/Connexion/Room"; import { Room } from "../../../src/Connexion/Room";
import {flattenGroupLayersMap} from "../../../src/Phaser/Map/LayersFlattener"; import { flattenGroupLayersMap } from "../../../src/Phaser/Map/LayersFlattener";
import type {ITiledMapLayer} from "../../../src/Phaser/Map/ITiledMap"; import type { ITiledMapLayer } from "../../../src/Phaser/Map/ITiledMap";
describe("Layers flattener", () => { describe("Layers flattener", () => {
it("should iterate maps with no group", () => { it("should iterate maps with no group", () => {
let flatLayers:ITiledMapLayer[] = []; let flatLayers: ITiledMapLayer[] = [];
flatLayers = flattenGroupLayersMap({ flatLayers = flattenGroupLayersMap({
"compressionlevel": -1, compressionlevel: -1,
"height": 2, height: 2,
"infinite": false, infinite: false,
"layers": [ layers: [
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 1, id: 1,
"name": "Tile Layer 1", name: "Tile Layer 1",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}, },
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 1, id: 1,
"name": "Tile Layer 2", name: "Tile Layer 2",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}], },
"nextlayerid": 2, ],
"nextobjectid": 1, nextlayerid: 2,
"orientation": "orthogonal", nextobjectid: 1,
"renderorder": "right-down", orientation: "orthogonal",
"tiledversion": "2021.03.23", renderorder: "right-down",
"tileheight": 32, tiledversion: "2021.03.23",
"tilesets": [], tileheight: 32,
"tilewidth": 32, tilesets: [],
"type": "map", tilewidth: 32,
"version": 1.5, type: "map",
"width": 2 version: 1.5,
}) width: 2,
});
const layers = []; const layers = [];
for (const layer of flatLayers) { for (const layer of flatLayers) {
layers.push(layer.name); layers.push(layer.name);
} }
expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']); expect(layers).toEqual(["Tile Layer 1", "Tile Layer 2"]);
}); });
it("should iterate maps with recursive groups", () => { it("should iterate maps with recursive groups", () => {
let flatLayers:ITiledMapLayer[] = []; let flatLayers: ITiledMapLayer[] = [];
flatLayers = flattenGroupLayersMap({ flatLayers = flattenGroupLayersMap({
"compressionlevel": -1, compressionlevel: -1,
"height": 2, height: 2,
"infinite": false, infinite: false,
"layers": [ layers: [
{ {
"id": 6, id: 6,
"layers": [ layers: [
{ {
"id": 5, id: 5,
"layers": [ layers: [
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 10, id: 10,
"name": "Tile3", name: "Tile3",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}, },
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 9, id: 9,
"name": "Tile2", name: "Tile2",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}], },
"name": "Group 3", ],
"opacity": 1, name: "Group 3",
"type": "group", opacity: 1,
"visible": true, type: "group",
"x": 0, visible: true,
"y": 0 x: 0,
y: 0,
}, },
{ {
"id": 7, id: 7,
"layers": [ layers: [
{ {
"data": [0, 0, 0, 0], data: [0, 0, 0, 0],
"height": 2, height: 2,
"id": 8, id: 8,
"name": "Tile1", name: "Tile1",
"opacity": 1, opacity: 1,
"type": "tilelayer", type: "tilelayer",
"visible": true, visible: true,
"width": 2, width: 2,
"x": 0, x: 0,
"y": 0 y: 0,
}], },
"name": "Group 2", ],
"opacity": 1, name: "Group 2",
"type": "group", opacity: 1,
"visible": true, type: "group",
"x": 0, visible: true,
"y": 0 x: 0,
}], y: 0,
"name": "Group 1", },
"opacity": 1, ],
"type": "group", name: "Group 1",
"visible": true, opacity: 1,
"x": 0, type: "group",
"y": 0 visible: true,
}], x: 0,
"nextlayerid": 11, y: 0,
"nextobjectid": 1, },
"orientation": "orthogonal", ],
"renderorder": "right-down", nextlayerid: 11,
"tiledversion": "2021.03.23", nextobjectid: 1,
"tileheight": 32, orientation: "orthogonal",
"tilesets": [], renderorder: "right-down",
"tilewidth": 32, tiledversion: "2021.03.23",
"type": "map", tileheight: 32,
"version": 1.5, tilesets: [],
"width": 2 tilewidth: 32,
}) type: "map",
version: 1.5,
width: 2,
});
const layers = []; const layers = [];
for (const layer of flatLayers) { for (const layer of flatLayers) {
layers.push(layer.name); layers.push(layer.name);
} }
expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']); expect(layers).toEqual(["Group 1/Group 3/Tile3", "Group 1/Group 3/Tile2", "Group 1/Group 2/Tile1"]);
}); });
}); });

View File

@ -7,7 +7,6 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin";
import sveltePreprocess from "svelte-preprocess"; import sveltePreprocess from "svelte-preprocess";
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
import { DISPLAY_TERMS_OF_USE } from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? "development"; const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS; const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;

View File

@ -262,10 +262,10 @@
"@types/mime" "^1" "@types/mime" "^1"
"@types/node" "*" "@types/node" "*"
"@types/simple-peer@^9.6.0": "@types/simple-peer@^9.11.1":
version "9.6.3" version "9.11.1"
resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.6.3.tgz#aa118a57e036f4ce2059a7e25367526a4764206d" resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.11.1.tgz#bef6ff1e75178d83438e33aa6a4df2fd98fded1d"
integrity sha512-zrXEBch9tF4NgkZDsGR3c1D0kq99M1bBCjzEyL0PVfEWzCIXrK64TuxRz3XKOx1B0KoEQ9kTs+AhMDuQaHy5RQ== integrity sha512-Pzqbau/WlivSXdRC0He2Wz/ANj2wbi4gzJrtysZz93jvOyI2jo/ibMjUe6AvPllFl/UO6QXT/A0Rcp44bDQB5A==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
@ -5008,7 +5008,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
simple-peer@^9.6.2: simple-peer@^9.11.0:
version "9.11.0" version "9.11.0"
resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571" resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571"
integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg== integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg==

View File

@ -1,11 +1,4 @@
{ "compressionlevel":-1, { "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":26, "height":26,
"infinite":false, "infinite":false,
"layers":[ "layers":[
@ -101,7 +94,7 @@
"opacity":1, "opacity":1,
"properties":[ "properties":[
{ {
"name":"exitSceneUrl", "name":"exitUrl",
"type":"string", "type":"string",
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs" "value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs"
}], }],
@ -119,7 +112,7 @@
"opacity":1, "opacity":1,
"properties":[ "properties":[
{ {
"name":"exitSceneUrl", "name":"exitUrl",
"type":"string", "type":"string",
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs-secours" "value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs-secours"
}], }],
@ -264,7 +257,7 @@
"nextobjectid":1, "nextobjectid":1,
"orientation":"orthogonal", "orientation":"orthogonal",
"renderorder":"right-down", "renderorder":"right-down",
"tiledversion":"1.3.3", "tiledversion":"2021.03.23",
"tileheight":32, "tileheight":32,
"tilesets":[ "tilesets":[
{ {
@ -1959,6 +1952,6 @@
}], }],
"tilewidth":32, "tilewidth":32,
"type":"map", "type":"map",
"version":1.2, "version":1.5,
"width":46 "width":46
} }

Some files were not shown because too many files have changed in this diff Show More