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

# Conflicts:
#	front/package.json
#	front/src/Components/App.svelte
#	front/src/Phaser/Game/GameScene.ts
#	front/src/Phaser/Menu/MenuScene.ts
#	front/src/WebRtc/MediaManager.ts
This commit is contained in:
David Négrier 2021-06-24 10:49:55 +02:00
commit e4708149e0
172 changed files with 15268 additions and 2850 deletions

71
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ develop ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ develop ]
schedule:
- cron: '24 17 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View file

@ -64,6 +64,11 @@ jobs:
run: yarn test run: yarn test
working-directory: "front" working-directory: "front"
# We will enable prettier checks on front in a few month, when most PRs without prettier have been merged
# - name: "Prettier"
# run: yarn run pretty-check
# working-directory: "front"
continuous-integration-pusher: continuous-integration-pusher:
name: "Continuous Integration Pusher" name: "Continuous Integration Pusher"
@ -107,6 +112,10 @@ jobs:
run: yarn test run: yarn test
working-directory: "pusher" working-directory: "pusher"
- name: "Prettier"
run: yarn run pretty-check
working-directory: "pusher"
continuous-integration-back: continuous-integration-back:
name: "Continuous Integration Back" name: "Continuous Integration Back"
@ -150,3 +159,7 @@ jobs:
run: yarn test run: yarn test
working-directory: "back" working-directory: "back"
- name: "Prettier"
run: yarn run pretty-check
working-directory: "back"

3
.gitignore vendored
View file

@ -6,4 +6,5 @@ docker-compose.override.yaml
*.DS_Store *.DS_Store
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/

1
.husky/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_

15
.husky/pre-commit Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
(
cd front || exit
yarn run precommit
)
(
cd pusher || exit
yarn run precommit
)
(
cd back || exit
yarn run precommit
)

View file

@ -1,4 +1,28 @@
## Version 1.3.9 - in dev ## Version 1.4.x-dev
### Updates
- Added the ability to have animated tiles in maps #1216 #1217
- Enabled outlines on actionable item again (they were disabled when migrating to Phaser 3.50) #1218
- Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219
- Migrated the admin console to Svelte, and redesigned the console #1211
- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1)
- New scripting API features :
- Use `WA.room.showLayer(): void` to show a layer
- Use `WA.room.hideLayer(): void` to hide a layer
- Use `WA.room.setProperty() : void` to add or change existing property of a layer
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
- Use `WA.room.getCurrentUser(): Promise<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.ui.registerMenuCommand(): void` to add a custom menu
## Version 1.4.1
### Bugfixes
- Loading errors after the preload stage should not crash the game anymore
## Version 1.4.0
### BREAKING CHANGES ### BREAKING CHANGES
@ -34,6 +58,7 @@
- New scripting API features: - New scripting API features:
- Use `WA.loadSound(): Sound` to load / play / stop a sound - Use `WA.loadSound(): Sound` to load / play / stop a sound
### Bug Fixes ### Bug Fixes
- Pinch gesture does no longer move the character - Pinch gesture does no longer move the character

67
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,67 @@
# Contributing to WorkAdventure
Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to
ask questions and how to work on something.
## Contributions we are seeking
We love to receive contributions from our community — you!
There are many ways to contribute, from writing tutorials or blog posts, improving the documentation,
submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself.
## Using the issue tracker
First things first: **Do NOT report security vulnerabilities in public issues!**.
Please read the [security guide](SECURITY.md) to learn who to do a security disclosure to the WorkAdventure core team.
You can use [GitHub issue tracker](https://github.com/thecodingmachine/workadventure/issues) to:
- File bug reports
- Ask for feature requests
If you have more general questions, a good place to ask is [our Discord server](https://discord.gg/YGtngdh9gt).
Finally, you can come and talk to the WorkAdventure core team... on WorkAdventure, of course! [Our offices are here](https://play.staging.workadventu.re/@/tcm/workadventure/wa-village).
## Pull requests
Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope
and avoid containing unrelated commits.
Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code),
otherwise you risk spending a lot of time working on something that the project's developers might not want to merge
into the project.
You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues).
### Linting your code
Before committing, be sure to install the "Prettier" precommit hook that will reformat your code to our coding style.
In order to enable the "Prettier" precommit hook, at the root of the project, run:
```console
$ yarn run install
$ yarn run prepare
```
If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need
to run code linting manually:
```console
$ docker-compose exec front yarn run pretty
$ docker-compose exec pusher yarn run pretty
$ docker-compose exec back yarn run pretty
```
### Providing tests
WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test.
Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine).
If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain
some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file
to add a reference to your newly created test map.

20
SECURITY.md Normal file
View file

@ -0,0 +1,20 @@
# Security Policy
## Reporting a Vulnerability
First things first: **Do NOT report security vulnerabilities in public issues!**
Please disclose responsibly by sending
a mail at security@workadventu.re (you can also ping us in the GitHub issues, but please, no details in the issues!)
We will assess the issue as soon as possible on a best-effort basis and will give you an estimate for when we have a fix
and release available for an eventual public disclosure.
We do not have a bug bounty program.
## Supported Versions
We only apply security patches on the latest tagged release and on the `master` and `develop` branches
Unless specified otherwise, do not expect us to fix security issues on past releases. We are only maintaining one release:
the latest one, which is online at https://play.workadventu.re.

1
back/.prettierignore Normal file
View file

@ -0,0 +1 @@
src/Messages/generated

4
back/.prettierrc.json Normal file
View file

@ -0,0 +1,4 @@
{
"printWidth": 120,
"tabWidth": 4
}

View file

@ -10,8 +10,11 @@
"runprod": "node --max-old-space-size=4096 ./dist/server.js", "runprod": "node --max-old-space-size=4096 ./dist/server.js",
"profile": "tsc && node --prof ./dist/server.js", "profile": "tsc && node --prof ./dist/server.js",
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts", "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts" "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts",
"precommit": "lint-staged",
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -66,7 +69,14 @@
"@typescript-eslint/parser": "^2.26.0", "@typescript-eslint/parser": "^2.26.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"jasmine": "^3.5.0", "jasmine": "^3.5.0",
"lint-staged": "^11.0.0",
"prettier": "^2.3.1",
"ts-node-dev": "^1.0.0-pre.44", "ts-node-dev": "^1.0.0-pre.44",
"typescript": "^3.8.3" "typescript": "^3.8.3"
},
"lint-staged": {
"*.ts": [
"prettier --write"
]
} }
} }

View file

@ -1,7 +1,7 @@
// lib/app.ts // lib/app.ts
import {PrometheusController} from "./Controller/PrometheusController"; import { PrometheusController } from "./Controller/PrometheusController";
import {DebugController} from "./Controller/DebugController"; import { DebugController } from "./Controller/DebugController";
import {App as uwsApp} from "./Server/sifrr.server"; import { App as uwsApp } from "./Server/sifrr.server";
class App { class App {
public app: uwsApp; public app: uwsApp;

View file

@ -1,10 +1,9 @@
import {HttpResponse} from "uWebSockets.js"; import { HttpResponse } from "uWebSockets.js";
export class BaseController { export class BaseController {
protected addCorsHeaders(res: HttpResponse): void { protected addCorsHeaders(res: HttpResponse): void {
res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept'); res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
res.writeHeader('access-control-allow-origin', '*'); res.writeHeader("access-control-allow-origin", "*");
} }
} }

View file

@ -1,53 +1,54 @@
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
import {stringify} from "circular-json"; import { stringify } from "circular-json";
import {HttpRequest, HttpResponse} from "uWebSockets.js"; import { HttpRequest, HttpResponse } from "uWebSockets.js";
import { parse } from 'query-string'; import { parse } from "query-string";
import {App} from "../Server/sifrr.server"; import { App } from "../Server/sifrr.server";
import {socketManager} from "../Services/SocketManager"; import { socketManager } from "../Services/SocketManager";
export class DebugController { export class DebugController {
constructor(private App : App) { constructor(private App: App) {
this.getDump(); this.getDump();
} }
getDump() {
getDump(){
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
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.status(401).send("Invalid token sent!");
} }
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( return res
socketManager.getWorlds(), .writeStatus("200 OK")
(key: unknown, value: unknown) => { .writeHeader("Content-Type", "application/json")
if (key === 'listeners') { .end(
return 'Listeners'; stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
} if (key === "listeners") {
if (key === 'socket') { return "Listeners";
return 'Socket';
}
if (key === 'batchedMessages') {
return 'BatchedMessages';
}
if(value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
} }
return obj; if (key === "socket") {
} else if(value instanceof Set) { return "Socket";
}
if (key === "batchedMessages") {
return "BatchedMessages";
}
if (value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
}
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = []; const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) { for (const [setKey, setValue] of value.entries()) {
obj.push(setValue); obj.push(setValue);
} }
return obj; return obj;
} else { } else {
return value; return value;
} }
} })
)); );
}); });
} }
} }

View file

@ -1,7 +1,7 @@
import {App} from "../Server/sifrr.server"; import { App } from "../Server/sifrr.server";
import {HttpRequest, HttpResponse} from "uWebSockets.js"; import { HttpRequest, HttpResponse } from "uWebSockets.js";
const register = require('prom-client').register; const register = require("prom-client").register;
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; const collectDefaultMetrics = require("prom-client").collectDefaultMetrics;
export class PrometheusController { export class PrometheusController {
constructor(private App: App) { constructor(private App: App) {
@ -14,7 +14,7 @@ export class PrometheusController {
} }
private metrics(res: HttpResponse, req: HttpRequest): void { private metrics(res: HttpResponse, req: HttpRequest): void {
res.writeHeader('Content-Type', register.contentType); res.writeHeader("Content-Type", register.contentType);
res.end(register.metrics()); res.end(register.metrics());
} }
} }

View file

@ -1,17 +1,17 @@
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || ''; const JITSI_ISS = process.env.JITSI_ISS || "";
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080; const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080;
const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051; const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ''; export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4'); export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export { export {
MINIMUM_DISTANCE, MINIMUM_DISTANCE,
@ -24,5 +24,5 @@ export {
CPU_OVERHEAT_THRESHOLD, CPU_OVERHEAT_THRESHOLD,
JITSI_URL, JITSI_URL,
JITSI_ISS, JITSI_ISS,
SECRET_JITSI_KEY SECRET_JITSI_KEY,
} };

View file

@ -1,15 +1,12 @@
import { import {
ServerToAdminClientMessage, ServerToAdminClientMessage,
UserJoinedRoomMessage, UserLeftRoomMessage UserJoinedRoomMessage,
UserLeftRoomMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {AdminSocket} from "../RoomManager"; import { AdminSocket } from "../RoomManager";
export class Admin { export class Admin {
public constructor( public constructor(private readonly socket: AdminSocket) {}
private readonly socket: AdminSocket
) {
}
public sendUserJoin(uuid: string, name: string, ip: string): void { public sendUserJoin(uuid: string, name: string, ip: string): void {
const serverToAdminClientMessage = new ServerToAdminClientMessage(); const serverToAdminClientMessage = new ServerToAdminClientMessage();
@ -24,7 +21,7 @@ export class Admin {
this.socket.write(serverToAdminClientMessage); this.socket.write(serverToAdminClientMessage);
} }
public sendUserLeft(uuid: string/*, name: string, ip: string*/): void { public sendUserLeft(uuid: string /*, name: string, ip: string*/): void {
const serverToAdminClientMessage = new ServerToAdminClientMessage(); const serverToAdminClientMessage = new ServerToAdminClientMessage();
const userLeftRoomMessage = new UserLeftRoomMessage(); const userLeftRoomMessage = new UserLeftRoomMessage();

View file

@ -1,16 +1,16 @@
import {PointInterface} from "./Websocket/PointInterface"; import { PointInterface } from "./Websocket/PointInterface";
import {Group} from "./Group"; import { Group } from "./Group";
import {User, UserSocket} from "./User"; import { User, UserSocket } from "./User";
import {PositionInterface} from "_Model/PositionInterface"; 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 { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
import {arrayIntersect} from "../Services/ArrayHelper"; 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";
import {Admin} from "../Model/Admin"; 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;
@ -39,33 +39,33 @@ export class GameRoom {
private readonly positionNotifier: PositionNotifier; private readonly positionNotifier: PositionNotifier;
public readonly roomId: string; public readonly roomId: string;
public readonly roomSlug: string; public readonly roomSlug: string;
public readonly worldSlug: string = ''; public readonly worldSlug: string = "";
public readonly organizationSlug: string = ''; public readonly organizationSlug: string = "";
private versionNumber:number = 1; private versionNumber: number = 1;
private nextUserId: number = 1; private nextUserId: number = 1;
constructor(roomId: string, constructor(
connectCallback: ConnectCallback, roomId: string,
disconnectCallback: DisconnectCallback, connectCallback: ConnectCallback,
minDistance: number, disconnectCallback: DisconnectCallback,
groupRadius: number, minDistance: number,
onEnters: EntersCallback, groupRadius: number,
onMoves: MovesCallback, onEnters: EntersCallback,
onLeaves: LeavesCallback, onMoves: MovesCallback,
onEmote: EmoteCallback, onLeaves: LeavesCallback,
onEmote: EmoteCallback
) { ) {
this.roomId = roomId; this.roomId = roomId;
if (isRoomAnonymous(roomId)) { if (isRoomAnonymous(roomId)) {
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
} else { } else {
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
this.roomSlug = roomSlug; this.roomSlug = roomSlug;
this.organizationSlug = organizationSlug; this.organizationSlug = organizationSlug;
this.worldSlug = worldSlug; 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>();
this.admins = new Set<Admin>(); this.admins = new Set<Admin>();
@ -86,21 +86,22 @@ export class GameRoom {
return this.users; return this.users;
} }
public getUserByUuid(uuid: string): User|undefined { public getUserByUuid(uuid: string): User | undefined {
return this.usersByUuid.get(uuid); return this.usersByUuid.get(uuid);
} }
public getUserById(id: number): User|undefined { public getUserById(id: number): User | undefined {
return this.users.get(id); return this.users.get(id);
} }
public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User { public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User {
const positionMessage = joinRoomMessage.getPositionmessage(); const positionMessage = joinRoomMessage.getPositionmessage();
if (positionMessage === undefined) { if (positionMessage === undefined) {
throw new Error('Missing position message'); throw new Error("Missing position message");
} }
const position = ProtobufUtils.toPointInterface(positionMessage); const position = ProtobufUtils.toPointInterface(positionMessage);
const user = new User(this.nextUserId, const user = new User(
this.nextUserId,
joinRoomMessage.getUseruuid(), joinRoomMessage.getUseruuid(),
joinRoomMessage.getIpaddress(), joinRoomMessage.getIpaddress(),
position, position,
@ -126,12 +127,12 @@ export class GameRoom {
return user; return user;
} }
public leave(user : User){ public leave(user: User) {
const userObj = this.users.get(user.id); const userObj = this.users.get(user.id);
if (userObj === undefined) { if (userObj === undefined) {
console.warn('User ', user.id, 'does not belong to this game room! It should!'); console.warn("User ", user.id, "does not belong to this game room! It should!");
} }
if (userObj !== undefined && typeof userObj.group !== 'undefined') { if (userObj !== undefined && typeof userObj.group !== "undefined") {
this.leaveGroup(userObj); this.leaveGroup(userObj);
} }
this.users.delete(user.id); this.users.delete(user.id);
@ -143,7 +144,7 @@ export class GameRoom {
// Notify admins // Notify admins
for (const admin of this.admins) { for (const admin of this.admins) {
admin.sendUserLeft(user.uuid/*, user.name, user.IPAddress*/); admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/);
} }
} }
@ -151,7 +152,7 @@ export class GameRoom {
return this.users.size === 0 && this.admins.size === 0; return this.users.size === 0 && this.admins.size === 0;
} }
public updatePosition(user : User, userPosition: PointInterface): void { public updatePosition(user: User, userPosition: PointInterface): void {
user.setPosition(userPosition); user.setPosition(userPosition);
this.updateUserGroup(user); this.updateUserGroup(user);
@ -173,22 +174,24 @@ export class GameRoom {
return; return;
} }
const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user); const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user);
if (closestItem !== null) { if (closestItem !== null) {
if (closestItem instanceof Group) { if (closestItem instanceof Group) {
// Let's join the group! // Let's join the group!
closestItem.join(user); closestItem.join(user);
} else { } else {
const closestUser : User = closestItem; const closestUser: User = closestItem;
const group: Group = new Group(this.roomId,[ const group: Group = new Group(
user, this.roomId,
closestUser [user, closestUser],
], this.connectCallback, this.disconnectCallback, this.positionNotifier); this.connectCallback,
this.disconnectCallback,
this.positionNotifier
);
this.groups.add(group); this.groups.add(group);
} }
} }
} else { } else {
// If the user is part of a group: // If the user is part of a group:
// should he leave the group? // should he leave the group?
@ -229,7 +232,9 @@ export class GameRoom {
this.positionNotifier.leave(group); this.positionNotifier.leave(group);
group.destroy(); group.destroy();
if (!this.groups.has(group)) { if (!this.groups.has(group)) {
throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World."); throw new Error(
"Could not find group " + group.getId() + " referenced by user " + user.id + " in World."
);
} }
this.groups.delete(group); this.groups.delete(group);
//todo: is the group garbage collected? //todo: is the group garbage collected?
@ -247,16 +252,15 @@ export class GameRoom {
* OR * OR
* - close enough to a group (distance <= groupRadius) * - close enough to a group (distance <= groupRadius)
*/ */
private searchClosestAvailableUserOrGroup(user: User): User|Group|null private searchClosestAvailableUserOrGroup(user: User): User | Group | null {
{
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius); let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
let matchingItem: User | Group | null = null; let matchingItem: User | Group | null = null;
this.users.forEach((currentUser, userId) => { this.users.forEach((currentUser, userId) => {
// Let's only check users that are not part of a group // Let's only check users that are not part of a group
if (typeof currentUser.group !== 'undefined') { if (typeof currentUser.group !== "undefined") {
return; return;
} }
if(currentUser === user) { if (currentUser === user) {
return; return;
} }
if (currentUser.silent) { if (currentUser.silent) {
@ -265,7 +269,7 @@ export class GameRoom {
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
if(distance <= minimumDistanceFound && distance <= this.minDistance) { if (distance <= minimumDistanceFound && distance <= this.minDistance) {
minimumDistanceFound = distance; minimumDistanceFound = distance;
matchingItem = currentUser; matchingItem = currentUser;
} }
@ -276,7 +280,7 @@ export class GameRoom {
return; return;
} }
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
if(distance <= minimumDistanceFound && distance <= this.groupRadius) { if (distance <= minimumDistanceFound && distance <= this.groupRadius) {
minimumDistanceFound = distance; minimumDistanceFound = distance;
matchingItem = group; matchingItem = group;
} }
@ -285,15 +289,15 @@ export class GameRoom {
return matchingItem; return matchingItem;
} }
public static computeDistance(user1: User, user2: User): number public static computeDistance(user1: User, user2: User): number {
{
const user1Position = user1.getPosition(); const user1Position = user1.getPosition();
const user2Position = user2.getPosition(); const user2Position = user2.getPosition();
return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)); return Math.sqrt(
Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)
);
} }
public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number {
{
return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2));
} }
@ -325,9 +329,9 @@ export class GameRoom {
public adminLeave(admin: Admin): void { public adminLeave(admin: Admin): void {
this.admins.delete(admin); this.admins.delete(admin);
} }
public incrementVersion(): number { public incrementVersion(): number {
this.versionNumber++ this.versionNumber++;
return this.versionNumber; return this.versionNumber;
} }

View file

@ -1,13 +1,12 @@
import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { ConnectCallback, DisconnectCallback } from "./GameRoom";
import { User } from "./User"; import { User } from "./User";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import {Movable} from "_Model/Movable"; import { Movable } from "_Model/Movable";
import {PositionNotifier} from "_Model/PositionNotifier"; import { PositionNotifier } from "_Model/PositionNotifier";
import {gaugeManager} from "../Services/GaugeManager"; import { gaugeManager } from "../Services/GaugeManager";
import {MAX_PER_GROUP} from "../Enum/EnvironmentVariable"; import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
export class Group implements Movable { export class Group implements Movable {
private static nextId: number = 1; private static nextId: number = 1;
private id: number; private id: number;
@ -18,8 +17,13 @@ export class Group implements Movable {
private wasDestroyed: boolean = false; private wasDestroyed: boolean = false;
private roomId: string; private roomId: string;
constructor(
constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { roomId: string,
users: User[],
private connectCallback: ConnectCallback,
private disconnectCallback: DisconnectCallback,
private positionNotifier: PositionNotifier
) {
this.roomId = roomId; this.roomId = roomId;
this.users = new Set<User>(); this.users = new Set<User>();
this.id = Group.nextId; this.id = Group.nextId;
@ -43,7 +47,7 @@ export class Group implements Movable {
return Array.from(this.users.values()); return Array.from(this.users.values());
} }
getId() : number { getId(): number {
return this.id; return this.id;
} }
@ -53,7 +57,7 @@ export class Group implements Movable {
getPosition(): PositionInterface { getPosition(): PositionInterface {
return { return {
x: this.x, x: this.x,
y: this.y y: this.y,
}; };
} }
@ -83,7 +87,7 @@ export class Group implements Movable {
if (oldX === undefined) { if (oldX === undefined) {
this.positionNotifier.enter(this); this.positionNotifier.enter(this);
} else { } else {
this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY}); this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY });
} }
} }
@ -95,19 +99,17 @@ export class Group implements Movable {
return this.users.size <= 1; return this.users.size <= 1;
} }
join(user: User): void join(user: User): void {
{
// Broadcast on the right event // Broadcast on the right event
this.connectCallback(user, this); this.connectCallback(user, this);
this.users.add(user); this.users.add(user);
user.group = this; user.group = this;
} }
leave(user: User): void leave(user: User): void {
{
const success = this.users.delete(user); const success = this.users.delete(user);
if (success === false) { if (success === false) {
throw new Error("Could not find user "+user.id+" in the group "+this.id); throw new Error("Could not find user " + user.id + " in the group " + this.id);
} }
user.group = undefined; user.group = undefined;
@ -123,8 +125,7 @@ export class Group implements Movable {
* Let's kick everybody out. * Let's kick everybody out.
* Usually used when there is only one user left. * Usually used when there is only one user left.
*/ */
destroy(): void destroy(): void {
{
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId); if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
for (const user of this.users) { for (const user of this.users) {
this.leave(user); this.leave(user);
@ -132,7 +133,7 @@ export class Group implements Movable {
this.wasDestroyed = true; this.wasDestroyed = true;
} }
get getSize(){ get getSize() {
return this.users.size; return this.users.size;
} }
} }

View file

@ -1,8 +1,8 @@
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
/** /**
* A physical object that can be placed into a Zone * A physical object that can be placed into a Zone
*/ */
export interface Movable { export interface Movable {
getPosition(): PositionInterface getPosition(): PositionInterface;
} }

View file

@ -1,4 +1,4 @@
export interface PositionInterface { export interface PositionInterface {
x: number, x: number;
y: number y: number;
} }

View file

@ -8,12 +8,12 @@
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
* number of players around the current player. * number of players around the current player.
*/ */
import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone"; import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone";
import {Movable} from "_Model/Movable"; import { Movable } from "_Model/Movable";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import {ZoneSocket} from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import {User} from "_Model/User"; import { User } from "_Model/User";
import {EmoteEventMessage} from "../Messages/generated/messages_pb"; import { EmoteEventMessage } from "../Messages/generated/messages_pb";
interface ZoneDescriptor { interface ZoneDescriptor {
i: number; i: number;
@ -21,19 +21,24 @@ interface ZoneDescriptor {
} }
export class PositionNotifier { export class PositionNotifier {
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
private zones: Zone[][] = []; private zones: Zone[][] = [];
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback, private onEmote: EmoteCallback) { constructor(
} private zoneWidth: number,
private zoneHeight: number,
private onUserEnters: EntersCallback,
private onUserMoves: MovesCallback,
private onUserLeaves: LeavesCallback,
private onEmote: EmoteCallback
) {}
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
return { return {
i: Math.floor(x / this.zoneWidth), i: Math.floor(x / this.zoneWidth),
j: Math.floor(y / this.zoneHeight), j: Math.floor(y / this.zoneHeight),
} };
} }
public enter(thing: Movable): void { public enter(thing: Movable): void {
@ -100,6 +105,5 @@ export class PositionNotifier {
const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y);
const zone = this.getZone(zoneDesc.i, zoneDesc.j); const zone = this.getZone(zoneDesc.i, zoneDesc.j);
zone.emitEmoteEvent(emoteEventMessage); zone.emitEmoteEvent(emoteEventMessage);
} }
} }

View file

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

View file

@ -1,11 +1,17 @@
import { Group } from "./Group"; import { Group } from "./Group";
import { PointInterface } from "./Websocket/PointInterface"; import { PointInterface } from "./Websocket/PointInterface";
import {Zone} from "_Model/Zone"; import { Zone } from "_Model/Zone";
import {Movable} from "_Model/Movable"; import { Movable } from "_Model/Movable";
import {PositionNotifier} from "_Model/PositionNotifier"; import { PositionNotifier } from "_Model/PositionNotifier";
import {ServerDuplexStream} from "grpc"; import { ServerDuplexStream } from "grpc";
import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; import {
import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; BatchMessage,
CompanionMessage,
PusherToBackMessage,
ServerToClientMessage,
SubMessage,
} from "../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>; export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
@ -22,7 +28,7 @@ export class User implements Movable {
private positionNotifier: PositionNotifier, private positionNotifier: PositionNotifier,
public readonly socket: UserSocket, public readonly socket: UserSocket,
public readonly tags: string[], public readonly tags: string[],
public readonly visitCardUrl: string|null, public readonly visitCardUrl: string | null,
public readonly name: string, public readonly name: string,
public readonly characterLayers: CharacterLayer[], public readonly characterLayers: CharacterLayer[],
public readonly companion?: CompanionMessage public readonly companion?: CompanionMessage
@ -42,9 +48,8 @@ export class User implements Movable {
this.positionNotifier.updatePosition(this, position, oldPosition); this.positionNotifier.updatePosition(this, position, oldPosition);
} }
private batchedMessages: BatchMessage = new BatchMessage(); private batchedMessages: BatchMessage = new BatchMessage();
private batchTimeout: NodeJS.Timeout|null = null; private batchTimeout: NodeJS.Timeout | null = null;
public emitInBatch(payload: SubMessage): void { public emitInBatch(payload: SubMessage): void {
this.batchedMessages.addPayload(payload); this.batchedMessages.addPayload(payload);

View file

@ -1,4 +1,4 @@
export interface CharacterLayer { export interface CharacterLayer {
name: string, name: string;
url: string|undefined url: string | undefined;
} }

View file

@ -1,10 +1,11 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
export const isItemEventMessageInterface = export const isItemEventMessageInterface = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
itemId: tg.isNumber, itemId: tg.isNumber,
event: tg.isString, event: tg.isString,
state: tg.isUnknown, state: tg.isUnknown,
parameters: tg.isUnknown, parameters: tg.isUnknown,
}).get(); })
.get();
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>; export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;

View file

@ -1,7 +1,10 @@
import {PointInterface} from "./PointInterface"; import { PointInterface } from "./PointInterface";
export class Point implements PointInterface{ export class Point implements PointInterface {
constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { constructor(
} public x: number,
public y: number,
public direction: string = "none",
public moving: boolean = false
) {}
} }

View file

@ -7,11 +7,12 @@ import * as tg from "generic-type-guard";
readonly moving: boolean; readonly moving: boolean;
}*/ }*/
export const isPointInterface = export const isPointInterface = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
x: tg.isNumber, x: tg.isNumber,
y: tg.isNumber, y: tg.isNumber,
direction: tg.isString, direction: tg.isString,
moving: tg.isBoolean moving: tg.isBoolean,
}).get(); })
.get();
export type PointInterface = tg.GuardedType<typeof isPointInterface>; export type PointInterface = tg.GuardedType<typeof isPointInterface>;

View file

@ -1,34 +1,33 @@
import {PointInterface} from "./PointInterface"; import { PointInterface } from "./PointInterface";
import { import {
CharacterLayerMessage, CharacterLayerMessage,
ItemEventMessage, ItemEventMessage,
PointMessage, PointMessage,
PositionMessage PositionMessage,
} from "../../Messages/generated/messages_pb"; } from "../../Messages/generated/messages_pb";
import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
export class ProtobufUtils { export class ProtobufUtils {
public static toPositionMessage(point: PointInterface): PositionMessage { public static toPositionMessage(point: PointInterface): PositionMessage {
let direction: Direction; let direction: Direction;
switch (point.direction) { switch (point.direction) {
case 'up': case "up":
direction = Direction.UP; direction = Direction.UP;
break; break;
case 'down': case "down":
direction = Direction.DOWN; direction = Direction.DOWN;
break; break;
case 'left': case "left":
direction = Direction.LEFT; direction = Direction.LEFT;
break; break;
case 'right': case "right":
direction = Direction.RIGHT; direction = Direction.RIGHT;
break; break;
default: default:
throw new Error('unexpected direction'); throw new Error("unexpected direction");
} }
const position = new PositionMessage(); const position = new PositionMessage();
@ -44,16 +43,16 @@ export class ProtobufUtils {
let direction: string; let direction: string;
switch (position.getDirection()) { switch (position.getDirection()) {
case Direction.UP: case Direction.UP:
direction = 'up'; direction = "up";
break; break;
case Direction.DOWN: case Direction.DOWN:
direction = 'down'; direction = "down";
break; break;
case Direction.LEFT: case Direction.LEFT:
direction = 'left'; direction = "left";
break; break;
case Direction.RIGHT: case Direction.RIGHT:
direction = 'right'; direction = "right";
break; break;
default: default:
throw new Error("Unexpected direction"); throw new Error("Unexpected direction");
@ -82,7 +81,7 @@ export class ProtobufUtils {
event: itemEventMessage.getEvent(), event: itemEventMessage.getEvent(),
parameters: JSON.parse(itemEventMessage.getParametersjson()), parameters: JSON.parse(itemEventMessage.getParametersjson()),
state: JSON.parse(itemEventMessage.getStatejson()), state: JSON.parse(itemEventMessage.getStatejson()),
} };
} }
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
@ -96,7 +95,7 @@ export class ProtobufUtils {
} }
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
return characterLayers.map(function(characterLayer): CharacterLayerMessage { return characterLayers.map(function (characterLayer): CharacterLayerMessage {
const message = new CharacterLayerMessage(); const message = new CharacterLayerMessage();
message.setName(characterLayer.name); message.setName(characterLayer.name);
if (characterLayer.url) { if (characterLayer.url) {
@ -107,7 +106,7 @@ export class ProtobufUtils {
} }
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] { public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
return characterLayers.map(function(characterLayer): CharacterLayer { return characterLayers.map(function (characterLayer): CharacterLayer {
const url = characterLayer.getUrl(); const url = characterLayer.getUrl();
return { return {
name: characterLayer.getName(), name: characterLayer.getName(),

View file

@ -1,35 +1,52 @@
import {User} from "./User"; import { User } from "./User";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import {Movable} from "./Movable"; import { Movable } from "./Movable";
import {Group} from "./Group"; import { Group } from "./Group";
import {ZoneSocket} from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import {EmoteEventMessage} from "../Messages/generated/messages_pb"; import { EmoteEventMessage } from "../Messages/generated/messages_pb";
export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void; export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void; export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
export class Zone { export class Zone {
private things: Set<Movable> = new Set<Movable>(); private things: Set<Movable> = new Set<Movable>();
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>(); private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
constructor(
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { } private onEnters: EntersCallback,
private onMoves: MovesCallback,
private onLeaves: LeavesCallback,
private onEmote: EmoteCallback,
public readonly x: number,
public readonly y: number
) {}
/** /**
* A user/thing leaves the zone * A user/thing leaves the zone
*/ */
public leave(thing: Movable, newZone: Zone|null) { public leave(thing: Movable, newZone: Zone | null) {
const result = this.things.delete(thing); const result = this.things.delete(thing);
if (!result) { if (!result) {
if (thing instanceof User) { if (thing instanceof User) {
throw new Error('Could not find user in zone '+thing.id); throw new Error("Could not find user in zone " + thing.id);
} }
if (thing instanceof Group) { if (thing instanceof Group) {
throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')'); throw new Error(
"Could not find group " +
thing.getId() +
" in zone (" +
this.x +
"," +
this.y +
"). Position of group: (" +
thing.getPosition().x +
"," +
thing.getPosition().y +
")"
);
} }
} }
this.notifyLeft(thing, newZone); this.notifyLeft(thing, newZone);
} }
@ -37,13 +54,13 @@ export class Zone {
/** /**
* Notify listeners of this zone that this user/thing left * Notify listeners of this zone that this user/thing left
*/ */
private notifyLeft(thing: Movable, newZone: Zone|null) { private notifyLeft(thing: Movable, newZone: Zone | null) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.onLeaves(thing, newZone, listener); this.onLeaves(thing, newZone, listener);
} }
} }
public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
this.things.add(thing); this.things.add(thing);
this.notifyEnter(thing, oldZone, position); this.notifyEnter(thing, oldZone, position);
} }
@ -51,13 +68,12 @@ export class Zone {
/** /**
* Notify listeners of this zone that this user entered * Notify listeners of this zone that this user entered
*/ */
private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.onEnters(thing, oldZone, listener); this.onEnters(thing, oldZone, listener);
} }
} }
public move(thing: Movable, position: PositionInterface) { public move(thing: Movable, position: PositionInterface) {
if (!this.things.has(thing)) { if (!this.things.has(thing)) {
this.things.add(thing); this.things.add(thing);
@ -67,7 +83,7 @@ export class Zone {
for (const listener of this.listeners) { for (const listener of this.listeners) {
//if (listener !== thing) { //if (listener !== thing) {
this.onMoves(thing,position, listener); this.onMoves(thing, position, listener);
//} //}
} }
} }
@ -89,6 +105,5 @@ export class Zone {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.onEmote(emoteEventMessage, listener); this.onEmote(emoteEventMessage, listener);
} }
} }
} }

View file

@ -1,4 +1,4 @@
import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb"; import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb";
import { import {
AdminGlobalMessage, AdminGlobalMessage,
AdminMessage, AdminMessage,
@ -11,92 +11,114 @@ import {
JoinRoomMessage, JoinRoomMessage,
PlayGlobalMessage, PlayGlobalMessage,
PusherToBackMessage, PusherToBackMessage,
QueryJitsiJwtMessage, RefreshRoomPromptMessage, QueryJitsiJwtMessage,
RefreshRoomPromptMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
ServerToClientMessage, ServerToClientMessage,
SilentMessage, SilentMessage,
UserMovesMessage, UserMovesMessage,
WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, WebRtcSignalToServerMessage,
ZoneMessage WorldFullWarningToRoomMessage,
ZoneMessage,
} from "./Messages/generated/messages_pb"; } from "./Messages/generated/messages_pb";
import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc"; import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
import {socketManager} from "./Services/SocketManager"; import { socketManager } from "./Services/SocketManager";
import {emitError} from "./Services/MessageHelpers"; import { emitError } from "./Services/MessageHelpers";
import {User, UserSocket} from "./Model/User"; import { User, UserSocket } from "./Model/User";
import {GameRoom} from "./Model/GameRoom"; import { GameRoom } from "./Model/GameRoom";
import Debug from "debug"; import Debug from "debug";
import {Admin} from "./Model/Admin"; import { Admin } from "./Model/Admin";
const debug = Debug('roommanager'); const debug = Debug("roommanager");
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>; export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>; export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
const roomManager: IRoomManagerServer = { const roomManager: IRoomManagerServer = {
joinRoom: (call: UserSocket): void => { joinRoom: (call: UserSocket): void => {
console.log('joinRoom called'); console.log("joinRoom called");
let room: GameRoom|null = null; let room: GameRoom | null = null;
let user: User|null = null; let user: User | null = null;
call.on('data', (message: PusherToBackMessage) => { call.on("data", (message: PusherToBackMessage) => {
try { try {
if (room === null || user === null) { if (room === null || user === null) {
if (message.hasJoinroommessage()) { if (message.hasJoinroommessage()) {
socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => { socketManager
if (call.writable) { .handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
room = gameRoom; .then(({ room: gameRoom, user: myUser }) => {
user = myUser; if (call.writable) {
} else { room = gameRoom;
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user. user = myUser;
socketManager.leaveRoom(gameRoom, myUser); } else {
} //Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
}); socketManager.leaveRoom(gameRoom, myUser);
}
});
} else { } else {
throw new Error('The first message sent MUST be of type JoinRoomMessage'); throw new Error("The first message sent MUST be of type JoinRoomMessage");
} }
} else { } else {
if (message.hasJoinroommessage()) { if (message.hasJoinroommessage()) {
throw new Error('Cannot call JoinRoomMessage twice!'); throw new Error("Cannot call JoinRoomMessage twice!");
} else if (message.hasUsermovesmessage()) { } else if (message.hasUsermovesmessage()) {
socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage); socketManager.handleUserMovesMessage(
room,
user,
message.getUsermovesmessage() as UserMovesMessage
);
} else if (message.hasSilentmessage()) { } else if (message.hasSilentmessage()) {
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) { } else if (message.hasItemeventmessage()) {
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage); socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
} else if (message.hasWebrtcsignaltoservermessage()) { } else if (message.hasWebrtcsignaltoservermessage()) {
socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); socketManager.emitVideo(
room,
user,
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) { } else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); socketManager.emitScreenSharing(
room,
user,
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasPlayglobalmessage()) { } else if (message.hasPlayglobalmessage()) {
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasQueryjitsijwtmessage()){ } else if (message.hasQueryjitsijwtmessage()) {
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); socketManager.handleQueryJitsiJwtMessage(
} else if (message.hasEmotepromptmessage()){ user,
socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage); message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
}else if (message.hasSendusermessage()) { );
} else if (message.hasEmotepromptmessage()) {
socketManager.handleEmoteEventMessage(
room,
user,
message.getEmotepromptmessage() as EmotePromptMessage
);
} else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage(); const sendUserMessage = message.getSendusermessage();
if(sendUserMessage !== undefined) { if (sendUserMessage !== undefined) {
socketManager.handlerSendUserMessage(user, sendUserMessage); socketManager.handlerSendUserMessage(user, sendUserMessage);
} }
}else if (message.hasBanusermessage()) { } else if (message.hasBanusermessage()) {
const banUserMessage = message.getBanusermessage(); const banUserMessage = message.getBanusermessage();
if(banUserMessage !== undefined) { if (banUserMessage !== undefined) {
socketManager.handlerBanUserMessage(room, user, banUserMessage); socketManager.handlerBanUserMessage(room, user, banUserMessage);
} }
} else { } else {
throw new Error('Unhandled message type'); throw new Error("Unhandled message type");
} }
} }
} catch (e) { } catch (e) {
emitError(call, e); emitError(call, e);
call.end(); call.end();
} }
}); });
call.on('end', () => { call.on("end", () => {
debug('joinRoom ended'); debug("joinRoom ended");
if (user !== null && room !== null) { if (user !== null && room !== null) {
socketManager.leaveRoom(room, user); socketManager.leaveRoom(room, user);
} }
@ -105,41 +127,40 @@ const roomManager: IRoomManagerServer = {
user = null; user = null;
}); });
call.on('error', (err: Error) => { call.on("error", (err: Error) => {
console.error('An error occurred in joinRoom stream:', err); console.error("An error occurred in joinRoom stream:", err);
}); });
}, },
listenZone(call: ZoneSocket): void { listenZone(call: ZoneSocket): void {
debug('listenZone called'); debug("listenZone called");
const zoneMessage = call.request; const zoneMessage = call.request;
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
call.on('cancelled', () => { call.on("cancelled", () => {
debug('listenZone cancelled'); debug("listenZone cancelled");
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
call.end(); call.end();
}) });
call.on('close', () => { call.on("close", () => {
debug('listenZone connection closed'); debug("listenZone connection closed");
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
}).on('error', (e) => { }).on("error", (e) => {
console.error('An error occurred in listenZone stream:', e); console.error("An error occurred in listenZone stream:", e);
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
call.end(); call.end();
}); });
}, },
adminRoom(call: AdminSocket): void { adminRoom(call: AdminSocket): void {
console.log('adminRoom called'); console.log("adminRoom called");
const admin = new Admin(call); const admin = new Admin(call);
let room: GameRoom|null = null; let room: GameRoom | null = null;
call.on('data', (message: AdminPusherToBackMessage) => { call.on("data", (message: AdminPusherToBackMessage) => {
try { try {
if (room === null) { if (room === null) {
if (message.hasSubscribetoroom()) { if (message.hasSubscribetoroom()) {
@ -148,18 +169,17 @@ const roomManager: IRoomManagerServer = {
room = gameRoom; room = gameRoom;
}); });
} else { } else {
throw new Error('The first message sent MUST be of type JoinRoomMessage'); throw new Error("The first message sent MUST be of type JoinRoomMessage");
} }
} }
} catch (e) { } catch (e) {
emitError(call, e); emitError(call, e);
call.end(); call.end();
} }
}); });
call.on('end', () => { call.on("end", () => {
debug('joinRoom ended'); debug("joinRoom ended");
if (room !== null) { if (room !== null) {
socketManager.leaveAdminRoom(room, admin); socketManager.leaveAdminRoom(room, admin);
} }
@ -167,18 +187,21 @@ const roomManager: IRoomManagerServer = {
room = null; room = null;
}); });
call.on('error', (err: Error) => { call.on("error", (err: Error) => {
console.error('An error occurred in joinAdminRoom stream:', err); console.error("An error occurred in joinAdminRoom stream:", err);
}); });
}, },
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void { sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
socketManager.sendAdminMessage(
socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage()); call.request.getRoomid(),
call.request.getRecipientuuid(),
call.request.getMessage()
);
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void { sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
throw new Error('Not implemented yet'); throw new Error("Not implemented yet");
// TODO // TODO
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
@ -192,14 +215,20 @@ const roomManager: IRoomManagerServer = {
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()); socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
sendWorldFullWarningToRoom(call: ServerUnaryCall<WorldFullWarningToRoomMessage>, callback: sendUnaryData<EmptyMessage>): void { sendWorldFullWarningToRoom(
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
callback: sendUnaryData<EmptyMessage>
): void {
socketManager.dispatchWorlFullWarning(call.request.getRoomid()); socketManager.dispatchWorlFullWarning(call.request.getRoomid());
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
sendRefreshRoomPrompt(call: ServerUnaryCall<RefreshRoomPromptMessage>, callback: sendUnaryData<EmptyMessage>): void { sendRefreshRoomPrompt(
call: ServerUnaryCall<RefreshRoomPromptMessage>,
callback: sendUnaryData<EmptyMessage>
): void {
socketManager.dispatchRoomRefresh(call.request.getRoomid()); socketManager.dispatchRoomRefresh(call.request.getRoomid());
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
}; };
export {roomManager}; export { roomManager };

View file

@ -1,13 +1,13 @@
import { App as _App, AppOptions } from 'uWebSockets.js'; import { App as _App, AppOptions } from "uWebSockets.js";
import BaseApp from './baseapp'; import BaseApp from "./baseapp";
import { extend } from './utils'; import { extend } from "./utils";
import { UwsApp } from './types'; import { UwsApp } from "./types";
class App extends (<UwsApp>_App) { class App extends (<UwsApp>_App) {
constructor(options: AppOptions = {}) { constructor(options: AppOptions = {}) {
super(options); // eslint-disable-line constructor-super super(options); // eslint-disable-line constructor-super
extend(this, new BaseApp()); extend(this, new BaseApp());
} }
} }
export default App; export default App;

View file

@ -1,116 +1,109 @@
import { Readable } from 'stream'; import { Readable } from "stream";
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
import formData from './formdata'; import formData from "./formdata";
import { stob } from './utils'; import { stob } from "./utils";
import { Handler } from './types'; import { Handler } from "./types";
import {join} from "path"; import { join } from "path";
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
const noOp = () => true; const noOp = () => true;
const handleBody = (res: HttpResponse, req: HttpRequest) => { const handleBody = (res: HttpResponse, req: HttpRequest) => {
const contType = req.getHeader('content-type'); const contType = req.getHeader("content-type");
res.bodyStream = function() { res.bodyStream = function () {
const stream = new Readable(); const stream = new Readable();
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
this.onData((ab: ArrayBuffer, isLast: boolean) => { this.onData((ab: ArrayBuffer, isLast: boolean) => {
// uint and then slicing is bit faster than slice and then uint // uint and then slicing is bit faster than slice and then uint
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
if (isLast) { if (isLast) {
stream.push(null); stream.push(null);
} }
}); });
return stream; return stream;
}; };
res.body = () => stob(res.bodyStream()); res.body = () => stob(res.bodyStream());
if (contType.includes('application/json')) if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
res.json = async () => JSON.parse(await res.body()); if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
if (contTypes.map(t => contType.includes(t)).includes(true))
res.formData = formData.bind(res, contType);
}; };
class BaseApp { class BaseApp {
_sockets = new Map(); _sockets = new Map();
ws!: TemplatedApp['ws']; ws!: TemplatedApp["ws"];
get!: TemplatedApp['get']; get!: TemplatedApp["get"];
_post!: TemplatedApp['post']; _post!: TemplatedApp["post"];
_put!: TemplatedApp['put']; _put!: TemplatedApp["put"];
_patch!: TemplatedApp['patch']; _patch!: TemplatedApp["patch"];
_listen!: TemplatedApp['listen']; _listen!: TemplatedApp["listen"];
post(pattern: string, handler: Handler) { post(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._post(pattern, (res, req) => {
this._post(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req); handler(res, req);
handler(res, req); });
}); return this;
return this; }
}
put(pattern: string, handler: Handler) { put(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._put(pattern, (res, req) => {
this._put(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req);
handler(res, req); handler(res, req);
}); });
return this; return this;
} }
patch(pattern: string, handler: Handler) { patch(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._patch(pattern, (res, req) => {
this._patch(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req);
handler(res, req); handler(res, req);
}); });
return this; return this;
} }
listen(h: string | number, p: Function | number = noOp, cb?: Function) { listen(h: string | number, p: Function | number = noOp, cb?: Function) {
if (typeof p === 'number' && typeof h === 'string') { if (typeof p === "number" && typeof h === "string") {
this._listen(h, p, socket => { this._listen(h, p, (socket) => {
this._sockets.set(p, socket); this._sockets.set(p, socket);
if (cb === undefined) { if (cb === undefined) {
throw new Error('cb undefined'); throw new Error("cb undefined");
}
cb(socket);
});
} else if (typeof h === "number" && typeof p === "function") {
this._listen(h, (socket) => {
this._sockets.set(h, socket);
p(socket);
});
} else {
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
} }
cb(socket);
}); return this;
} else if (typeof h === 'number' && typeof p === 'function') {
this._listen(h, socket => {
this._sockets.set(h, socket);
p(socket);
});
} else {
throw Error(
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
);
} }
return this; close(port: null | number = null) {
} if (port) {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
close(port: null | number = null) { this._sockets.delete(port);
if (port) { } else {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); this._sockets.forEach((app) => {
this._sockets.delete(port); us_listen_socket_close(app);
} else { });
this._sockets.forEach(app => { this._sockets.clear();
us_listen_socket_close(app); }
}); return this;
this._sockets.clear();
} }
return this;
}
} }
export default BaseApp; export default BaseApp;

View file

@ -1,100 +1,99 @@
import { createWriteStream } from 'fs'; import { createWriteStream } from "fs";
import { join, dirname } from 'path'; import { join, dirname } from "path";
import Busboy from 'busboy'; import Busboy from "busboy";
import mkdirp from 'mkdirp'; import mkdirp from "mkdirp";
function formData( function formData(
contType: string, contType: string,
options: busboy.BusboyConfig & { options: busboy.BusboyConfig & {
abortOnLimit?: boolean; abortOnLimit?: boolean;
tmpDir?: string; tmpDir?: string;
onFile?: ( onFile?: (
fieldname: string, fieldname: string,
file: NodeJS.ReadableStream, file: NodeJS.ReadableStream,
filename: string, filename: string,
encoding: string, encoding: string,
mimetype: string mimetype: string
) => string; ) => string;
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
filename?: (oldName: string) => string; filename?: (oldName: string) => string;
} = {} } = {}
) { ) {
console.log('Enter form data'); console.log("Enter form data");
options.headers = { options.headers = {
'content-type': contType "content-type": contType,
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const busb = new Busboy(options); const busb = new Busboy(options);
const ret = {}; const ret = {};
this.bodyStream().pipe(busb); this.bodyStream().pipe(busb);
busb.on('limit', () => { busb.on("limit", () => {
if (options.abortOnLimit) { if (options.abortOnLimit) {
reject(Error('limit')); reject(Error("limit"));
} }
});
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
filename,
encoding,
mimetype,
filePath: undefined,
};
if (typeof options.tmpDir === "string") {
if (typeof options.filename === "function") filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave;
}
if (typeof options.onFile === "function") {
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
}
setRetValue(ret, fieldname, value);
});
busb.on("field", function (fieldname, value) {
if (typeof options.onField === "function") options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on("finish", function () {
resolve(ret);
});
busb.on("error", reject);
}); });
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = {
filename,
encoding,
mimetype,
filePath: undefined
};
if (typeof options.tmpDir === 'string') {
if (typeof options.filename === 'function') filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave;
}
if (typeof options.onFile === 'function') {
value.filePath =
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
}
setRetValue(ret, fieldname, value);
});
busb.on('field', function(fieldname, value) {
if (typeof options.onField === 'function') options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on('finish', function() {
resolve(ret);
});
busb.on('error', reject);
});
} }
function setRetValue( function setRetValue(
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
fieldname: string, fieldname: string,
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
) { ) {
if (fieldname.endsWith('[]')) { if (fieldname.endsWith("[]")) {
fieldname = fieldname.slice(0, fieldname.length - 2); fieldname = fieldname.slice(0, fieldname.length - 2);
if (Array.isArray(ret[fieldname])) { if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value); ret[fieldname].push(value);
} else {
ret[fieldname] = [value];
}
} else { } else {
ret[fieldname] = [value]; if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else if (ret[fieldname]) {
ret[fieldname] = [ret[fieldname], value];
} else {
ret[fieldname] = value;
}
} }
} else {
if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else if (ret[fieldname]) {
ret[fieldname] = [ret[fieldname], value];
} else {
ret[fieldname] = value;
}
}
} }
export default formData; export default formData;

View file

@ -1,13 +1,13 @@
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
import BaseApp from './baseapp'; import BaseApp from "./baseapp";
import { extend } from './utils'; import { extend } from "./utils";
import { UwsApp } from './types'; import { UwsApp } from "./types";
class SSLApp extends (<UwsApp>_SSLApp) { class SSLApp extends (<UwsApp>_SSLApp) {
constructor(options: AppOptions) { constructor(options: AppOptions) {
super(options); // eslint-disable-line constructor-super super(options); // eslint-disable-line constructor-super
extend(this, new BaseApp()); extend(this, new BaseApp());
} }
} }
export default SSLApp; export default SSLApp;

View file

@ -1,9 +1,9 @@
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
export type UwsApp = { export type UwsApp = {
(options: AppOptions): TemplatedApp; (options: AppOptions): TemplatedApp;
new (options: AppOptions): TemplatedApp; new (options: AppOptions): TemplatedApp;
prototype: TemplatedApp; prototype: TemplatedApp;
}; };
export type Handler = (res: HttpResponse, req: HttpRequest) => void; export type Handler = (res: HttpResponse, req: HttpRequest) => void;

View file

@ -1,37 +1,36 @@
import { ReadStream } from 'fs'; import { ReadStream } from "fs";
function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( function extend(who: any, from: any, overwrite = true) {
Object.keys(from) const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
); ownProps.forEach((prop) => {
ownProps.forEach(prop => { if (prop === "constructor" || from[prop] === undefined) return;
if (prop === 'constructor' || from[prop] === undefined) return; if (who[prop] && overwrite) {
if (who[prop] && overwrite) { who[`_${prop}`] = who[prop];
who[`_${prop}`] = who[prop]; }
} if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); else who[prop] = from[prop];
else who[prop] = from[prop]; });
});
} }
function stob(stream: ReadStream): Promise<Buffer> { function stob(stream: ReadStream): Promise<Buffer> {
return new Promise(resolve => { return new Promise((resolve) => {
const buffers: Buffer[] = []; const buffers: Buffer[] = [];
stream.on('data', buffers.push.bind(buffers)); stream.on("data", buffers.push.bind(buffers));
stream.on('end', () => { stream.on("end", () => {
switch (buffers.length) { switch (buffers.length) {
case 0: case 0:
resolve(Buffer.allocUnsafe(0)); resolve(Buffer.allocUnsafe(0));
break; break;
case 1: case 1:
resolve(buffers[0]); resolve(buffers[0]);
break; break;
default: default:
resolve(Buffer.concat(buffers)); resolve(Buffer.concat(buffers));
} }
});
}); });
});
} }
export { extend, stob }; export { extend, stob };

View file

@ -1,19 +1,19 @@
import { parse } from 'query-string'; import { parse } from "query-string";
import { HttpRequest } from 'uWebSockets.js'; import { HttpRequest } from "uWebSockets.js";
import App from './server/app'; import App from "./server/app";
import SSLApp from './server/sslapp'; import SSLApp from "./server/sslapp";
import * as types from './server/types'; import * as types from "./server/types";
const getQuery = (req: HttpRequest) => { const getQuery = (req: HttpRequest) => {
return parse(req.getQuery()); return parse(req.getQuery());
}; };
export { App, SSLApp, getQuery }; export { App, SSLApp, getQuery };
export * from './server/types'; export * from "./server/types";
export default { export default {
App, App,
SSLApp, SSLApp,
getQuery, getQuery,
...types ...types,
}; };

View file

@ -1,3 +1,3 @@
export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { export const arrayIntersect = (array1: string[], array2: string[]): boolean => {
return array1.filter(value => array2.includes(value)).length > 0; return array1.filter((value) => array2.includes(value)).length > 0;
} };

View file

@ -1,7 +1,7 @@
const EventEmitter = require('events'); const EventEmitter = require("events");
const clientJoinEvent = 'clientJoin'; const clientJoinEvent = "clientJoin";
const clientLeaveEvent = 'clientLeave'; const clientLeaveEvent = "clientLeave";
class ClientEventsEmitter extends EventEmitter { class ClientEventsEmitter extends EventEmitter {
emitClientJoin(clientUUid: string, roomId: string): void { emitClientJoin(clientUUid: string, roomId: string): void {

View file

@ -1,6 +1,6 @@
import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
function secNSec2ms(secNSec: Array<number>|number) { function secNSec2ms(secNSec: Array<number> | number) {
if (Array.isArray(secNSec)) { if (Array.isArray(secNSec)) {
return secNSec[0] * 1000 + secNSec[1] / 1000000; return secNSec[0] * 1000 + secNSec[1] / 1000000;
} }
@ -12,17 +12,17 @@ class CpuTracker {
private overHeating: boolean = false; private overHeating: boolean = false;
constructor() { constructor() {
let time = process.hrtime.bigint() let time = process.hrtime.bigint();
let usage = process.cpuUsage() let usage = process.cpuUsage();
setInterval(() => { setInterval(() => {
const elapTime = process.hrtime.bigint(); const elapTime = process.hrtime.bigint();
const elapUsage = process.cpuUsage(usage) const elapUsage = process.cpuUsage(usage);
usage = process.cpuUsage() usage = process.cpuUsage();
const elapTimeMS = elapTime - time; const elapTimeMS = elapTime - time;
const elapUserMS = secNSec2ms(elapUsage.user) const elapUserMS = secNSec2ms(elapUsage.user);
const elapSystMS = secNSec2ms(elapUsage.system) const elapSystMS = secNSec2ms(elapUsage.system);
this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
time = elapTime; time = elapTime;

View file

@ -1,4 +1,4 @@
import {Counter, Gauge} from "prom-client"; import { Counter, Gauge } from "prom-client";
//this class should manage all the custom metrics used by prometheus //this class should manage all the custom metrics used by prometheus
class GaugeManager { class GaugeManager {
@ -10,29 +10,29 @@ class GaugeManager {
constructor() { constructor() {
this.nbRoomsGauge = new Gauge({ this.nbRoomsGauge = new Gauge({
name: 'workadventure_nb_rooms', name: "workadventure_nb_rooms",
help: 'Number of active rooms' help: "Number of active rooms",
}); });
this.nbClientsGauge = new Gauge({ this.nbClientsGauge = new Gauge({
name: 'workadventure_nb_sockets', name: "workadventure_nb_sockets",
help: 'Number of connected sockets', help: "Number of connected sockets",
labelNames: [ ] labelNames: [],
}); });
this.nbClientsPerRoomGauge = new Gauge({ this.nbClientsPerRoomGauge = new Gauge({
name: 'workadventure_nb_clients_per_room', name: "workadventure_nb_clients_per_room",
help: 'Number of clients per room', help: "Number of clients per room",
labelNames: [ 'room' ] labelNames: ["room"],
}); });
this.nbGroupsPerRoomCounter = new Counter({ this.nbGroupsPerRoomCounter = new Counter({
name: 'workadventure_counter_groups_per_room', name: "workadventure_counter_groups_per_room",
help: 'Counter of groups per room', help: "Counter of groups per room",
labelNames: [ 'room' ] labelNames: ["room"],
}); });
this.nbGroupsPerRoomGauge = new Gauge({ this.nbGroupsPerRoomGauge = new Gauge({
name: 'workadventure_nb_groups_per_room', name: "workadventure_nb_groups_per_room",
help: 'Number of groups per room', help: "Number of groups per room",
labelNames: [ 'room' ] labelNames: ["room"],
}); });
} }
@ -54,13 +54,13 @@ class GaugeManager {
} }
incNbGroupsPerRoomGauge(roomId: string): void { incNbGroupsPerRoomGauge(roomId: string): void {
this.nbGroupsPerRoomCounter.inc({ room: roomId }) this.nbGroupsPerRoomCounter.inc({ room: roomId });
this.nbGroupsPerRoomGauge.inc({ room: roomId }) this.nbGroupsPerRoomGauge.inc({ room: roomId });
} }
decNbGroupsPerRoomGauge(roomId: string): void { decNbGroupsPerRoomGauge(roomId: string): void {
this.nbGroupsPerRoomGauge.dec({ room: roomId }) this.nbGroupsPerRoomGauge.dec({ room: roomId });
} }
} }
export const gaugeManager = new GaugeManager(); export const gaugeManager = new GaugeManager();

View file

@ -1,5 +1,5 @@
import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb"; import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb";
import {UserSocket} from "_Model/User"; import { UserSocket } from "_Model/User";
export function emitError(Client: UserSocket, message: string): void { export function emitError(Client: UserSocket, message: string): void {
const errorMessage = new ErrorMessage(); const errorMessage = new ErrorMessage();
@ -9,7 +9,7 @@ export function emitError(Client: UserSocket, message: string): void {
serverToClientMessage.setErrormessage(errorMessage); serverToClientMessage.setErrormessage(errorMessage);
//if (!Client.disconnecting) { //if (!Client.disconnecting) {
Client.write(serverToClientMessage); Client.write(serverToClientMessage);
//} //}
console.warn(message); console.warn(message);
} }

View file

@ -1,4 +1,4 @@
import {GameRoom} from "../Model/GameRoom"; import { GameRoom } from "../Model/GameRoom";
import { import {
ItemEventMessage, ItemEventMessage,
ItemStateMessage, ItemStateMessage,
@ -27,39 +27,39 @@ import {
WorldFullWarningMessage, WorldFullWarningMessage,
UserLeftZoneMessage, UserLeftZoneMessage,
EmoteEventMessage, EmoteEventMessage,
BanUserMessage, RefreshRoomMessage, EmotePromptMessage, BanUserMessage,
RefreshRoomMessage,
EmotePromptMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {User, UserSocket} from "../Model/User"; import { User, UserSocket } from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import {Group} from "../Model/Group"; import { Group } from "../Model/Group";
import {cpuTracker} from "./CpuTracker"; import { cpuTracker } from "./CpuTracker";
import { import {
GROUP_RADIUS, GROUP_RADIUS,
JITSI_ISS, JITSI_ISS,
MINIMUM_DISTANCE, MINIMUM_DISTANCE,
SECRET_JITSI_KEY, SECRET_JITSI_KEY,
TURN_STATIC_AUTH_SECRET TURN_STATIC_AUTH_SECRET,
} from "../Enum/EnvironmentVariable"; } from "../Enum/EnvironmentVariable";
import {Movable} from "../Model/Movable"; import { Movable } from "../Model/Movable";
import {PositionInterface} from "../Model/PositionInterface"; import { PositionInterface } from "../Model/PositionInterface";
import Jwt from "jsonwebtoken"; import Jwt from "jsonwebtoken";
import {JITSI_URL} from "../Enum/EnvironmentVariable"; import { JITSI_URL } from "../Enum/EnvironmentVariable";
import {clientEventsEmitter} from "./ClientEventsEmitter"; import { clientEventsEmitter } from "./ClientEventsEmitter";
import {gaugeManager} from "./GaugeManager"; import { gaugeManager } from "./GaugeManager";
import {ZoneSocket} from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import {Zone} from "_Model/Zone"; import { Zone } from "_Model/Zone";
import Debug from "debug"; import Debug from "debug";
import {Admin} from "_Model/Admin"; import { Admin } from "_Model/Admin";
import crypto from "crypto"; import crypto from "crypto";
const debug = Debug("sockermanager");
const debug = Debug('sockermanager');
function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void { function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void {
// TODO: should we batch those every 100ms? // TODO: should we batch those every 100ms?
const batchMessage = new BatchToPusherMessage(); const batchMessage = new BatchToPusherMessage();
batchMessage.addPayload(subMessage); batchMessage.addPayload(subMessage);
socket.write(batchMessage); socket.write(batchMessage);
} }
@ -68,7 +68,6 @@ export class SocketManager {
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>(); private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
constructor() { constructor() {
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
gaugeManager.incNbClientPerRoomGauge(roomId); gaugeManager.incNbClientPerRoomGauge(roomId);
}); });
@ -77,16 +76,18 @@ export class SocketManager {
}); });
} }
public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { public async handleJoinRoom(
socket: UserSocket,
joinRoomMessage: JoinRoomMessage
): Promise<{ room: GameRoom; user: User }> {
//join new previous room //join new previous room
const {room, user} = await this.joinRoom(socket, joinRoomMessage); const { room, user } = await this.joinRoom(socket, joinRoomMessage);
if (!socket.writable) { if (!socket.writable) {
console.warn('Socket was aborted'); console.warn("Socket was aborted");
return { return {
room, room,
user user,
}; };
} }
const roomJoinedMessage = new RoomJoinedMessage(); const roomJoinedMessage = new RoomJoinedMessage();
@ -108,9 +109,8 @@ export class SocketManager {
return { return {
room, room,
user user,
}; };
} }
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) { handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
@ -124,13 +124,12 @@ export class SocketManager {
} }
if (position === undefined) { if (position === undefined) {
throw new Error('Position not found in message'); throw new Error("Position not found in message");
} }
const viewport = userMoves.viewport; const viewport = userMoves.viewport;
if (viewport === undefined) { if (viewport === undefined) {
throw new Error('Viewport not found in message'); throw new Error("Viewport not found in message");
} }
// update position in the world // update position in the world
room.updatePosition(user, ProtobufUtils.toPointInterface(position)); room.updatePosition(user, ProtobufUtils.toPointInterface(position));
@ -189,7 +188,11 @@ export class SocketManager {
//send only at user //send only at user
const remoteUser = room.getUsers().get(data.getReceiverid()); const remoteUser = room.getUsers().get(data.getReceiverid());
if (remoteUser === undefined) { if (remoteUser === undefined) {
console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); console.warn(
"While exchanging a WebRTC signal: client with id ",
data.getReceiverid(),
" does not exist. This might be a race condition."
);
return; return;
} }
@ -197,8 +200,8 @@ export class SocketManager {
webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal()); webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer" // TODO: only compute credentials if data.signal.type === "offer"
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);
webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password); webrtcSignalToClient.setWebrtcpassword(password);
} }
@ -207,7 +210,7 @@ export class SocketManager {
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
//if (!client.disconnecting) { //if (!client.disconnecting) {
remoteUser.socket.write(serverToClientMessage); remoteUser.socket.write(serverToClientMessage);
//} //}
} }
@ -215,7 +218,11 @@ export class SocketManager {
//send only at user //send only at user
const remoteUser = room.getUsers().get(data.getReceiverid()); const remoteUser = room.getUsers().get(data.getReceiverid());
if (remoteUser === undefined) { if (remoteUser === undefined) {
console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); console.warn(
"While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ",
data.getReceiverid(),
" does not exist. This might be a race condition."
);
return; return;
} }
@ -223,8 +230,8 @@ export class SocketManager {
webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal()); webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer" // TODO: only compute credentials if data.signal.type === "offer"
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);
webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password); webrtcSignalToClient.setWebrtcpassword(password);
} }
@ -233,11 +240,11 @@ export class SocketManager {
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
//if (!client.disconnecting) { //if (!client.disconnecting) {
remoteUser.socket.write(serverToClientMessage); remoteUser.socket.write(serverToClientMessage);
//} //}
} }
leaveRoom(room: GameRoom, user: User){ leaveRoom(room: GameRoom, user: User) {
// leave previous room and world // leave previous room and world
try { try {
//user leave previous world //user leave previous world
@ -249,33 +256,39 @@ export class SocketManager {
} }
} finally { } finally {
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
console.log('A user left'); console.log("A user left");
} }
} }
async getOrCreateRoom(roomId: string): Promise<GameRoom> { async getOrCreateRoom(roomId: string): Promise<GameRoom> {
//check and create new world for a room //check and create new world for a room
let world = this.rooms.get(roomId) let world = this.rooms.get(roomId);
if(world === undefined){ if (world === undefined) {
world = new GameRoom( world = new GameRoom(
roomId, roomId,
(user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.joinWebRtcRoom(user, group),
(user: User, group: Group) => this.disConnectedUser(user, group), (user: User, group: Group) => this.disConnectedUser(user, group),
MINIMUM_DISTANCE, MINIMUM_DISTANCE,
GROUP_RADIUS, GROUP_RADIUS,
(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener), (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
(thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), this.onZoneEnter(thing, fromZone, listener),
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener), (thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
(emoteEventMessage:EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener), this.onClientMove(thing, position, listener),
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
this.onClientLeave(thing, newZone, listener),
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
this.onEmote(emoteEventMessage, listener)
); );
gaugeManager.incNbRoomGauge(); gaugeManager.incNbRoomGauge();
this.rooms.set(roomId, world); this.rooms.set(roomId, world);
} }
return Promise.resolve(world) return Promise.resolve(world);
} }
private async joinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { private async joinRoom(
socket: UserSocket,
joinRoomMessage: JoinRoomMessage
): Promise<{ room: GameRoom; user: User }> {
const roomId = joinRoomMessage.getRoomid(); const roomId = joinRoomMessage.getRoomid();
const room = await socketManager.getOrCreateRoom(roomId); const room = await socketManager.getOrCreateRoom(roomId);
@ -284,15 +297,15 @@ export class SocketManager {
const user = room.join(socket, joinRoomMessage); const user = room.join(socket, joinRoomMessage);
clientEventsEmitter.emitClientJoin(user.uuid, roomId); clientEventsEmitter.emitClientJoin(user.uuid, roomId);
console.log(new Date().toISOString() + ' A user joined'); console.log(new Date().toISOString() + " A user joined");
return {room, user}; return { room, user };
} }
private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) { private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) {
if (thing instanceof User) { if (thing instanceof User) {
const userJoinedZoneMessage = new UserJoinedZoneMessage(); const userJoinedZoneMessage = new UserJoinedZoneMessage();
if (!Number.isInteger(thing.id)) { if (!Number.isInteger(thing.id)) {
throw new Error('clientUser.userId is not an integer '+thing.id); throw new Error("clientUser.userId is not an integer " + thing.id);
} }
userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setName(thing.name);
@ -312,11 +325,11 @@ export class SocketManager {
} else if (thing instanceof Group) { } else if (thing instanceof Group) {
this.emitCreateUpdateGroupEvent(listener, fromZone, thing); this.emitCreateUpdateGroupEvent(listener, fromZone, thing);
} else { } else {
console.error('Unexpected type for Movable.'); console.error("Unexpected type for Movable.");
} }
} }
private onClientMove(thing: Movable, position:PositionInterface, listener: ZoneSocket): void { private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void {
if (thing instanceof User) { if (thing instanceof User) {
const userMovedMessage = new UserMovedMessage(); const userMovedMessage = new UserMovedMessage();
userMovedMessage.setUserid(thing.id); userMovedMessage.setUserid(thing.id);
@ -331,21 +344,20 @@ export class SocketManager {
} else if (thing instanceof Group) { } else if (thing instanceof Group) {
this.emitCreateUpdateGroupEvent(listener, null, thing); this.emitCreateUpdateGroupEvent(listener, null, thing);
} else { } else {
console.error('Unexpected type for Movable.'); console.error("Unexpected type for Movable.");
} }
} }
private onClientLeave(thing: Movable, newZone: Zone|null, listener: ZoneSocket) { private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) {
if (thing instanceof User) { if (thing instanceof User) {
this.emitUserLeftEvent(listener, thing.id, newZone); this.emitUserLeftEvent(listener, thing.id, newZone);
} else if (thing instanceof Group) { } else if (thing instanceof Group) {
this.emitDeleteGroupEvent(listener, thing.getId(), newZone); this.emitDeleteGroupEvent(listener, thing.getId(), newZone);
} else { } else {
console.error('Unexpected type for Movable.'); console.error("Unexpected type for Movable.");
} }
} }
private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) { private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) {
const subMessage = new SubToPusherMessage(); const subMessage = new SubToPusherMessage();
subMessage.setEmoteeventmessage(emoteEventMessage); subMessage.setEmoteeventmessage(emoteEventMessage);
@ -353,7 +365,7 @@ export class SocketManager {
emitZoneMessage(subMessage, client); emitZoneMessage(subMessage, client);
} }
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void { private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
const position = group.getPosition(); const position = group.getPosition();
const pointMessage = new PointMessage(); const pointMessage = new PointMessage();
pointMessage.setX(Math.floor(position.x)); pointMessage.setX(Math.floor(position.x));
@ -371,7 +383,7 @@ export class SocketManager {
//client.emitInBatch(subMessage); //client.emitInBatch(subMessage);
} }
private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone|null): void { private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void {
const groupDeleteMessage = new GroupLeftZoneMessage(); const groupDeleteMessage = new GroupLeftZoneMessage();
groupDeleteMessage.setGroupid(groupId); groupDeleteMessage.setGroupid(groupId);
groupDeleteMessage.setTozone(this.toProtoZone(newZone)); groupDeleteMessage.setTozone(this.toProtoZone(newZone));
@ -383,7 +395,7 @@ export class SocketManager {
//user.emitInBatch(subMessage); //user.emitInBatch(subMessage);
} }
private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone|null): void { private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void {
const userLeftMessage = new UserLeftZoneMessage(); const userLeftMessage = new UserLeftZoneMessage();
userLeftMessage.setUserid(userId); userLeftMessage.setUserid(userId);
userLeftMessage.setTozone(this.toProtoZone(newZone)); userLeftMessage.setTozone(this.toProtoZone(newZone));
@ -394,7 +406,7 @@ export class SocketManager {
emitZoneMessage(subMessage, client); emitZoneMessage(subMessage, client);
} }
private toProtoZone(zone: Zone|null): ProtoZone|undefined { private toProtoZone(zone: Zone | null): ProtoZone | undefined {
if (zone !== null) { if (zone !== null) {
const zoneMessage = new ProtoZone(); const zoneMessage = new ProtoZone();
zoneMessage.setX(zone.x); zoneMessage.setX(zone.x);
@ -405,7 +417,6 @@ export class SocketManager {
} }
private joinWebRtcRoom(user: User, group: Group) { private joinWebRtcRoom(user: User, group: Group) {
for (const otherUser of group.getUsers()) { for (const otherUser of group.getUsers()) {
if (user === otherUser) { if (user === otherUser) {
continue; continue;
@ -416,8 +427,8 @@ export class SocketManager {
webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setName(otherUser.name); 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);
webrtcStartMessage1.setWebrtcusername(username); webrtcStartMessage1.setWebrtcusername(username);
webrtcStartMessage1.setWebrtcpassword(password); webrtcStartMessage1.setWebrtcpassword(password);
} }
@ -426,16 +437,16 @@ export class SocketManager {
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
//if (!user.socket.disconnecting) { //if (!user.socket.disconnecting) {
user.socket.write(serverToClientMessage1); user.socket.write(serverToClientMessage1);
//console.log('Sending webrtcstart initiator to '+user.socket.userId) //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.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);
webrtcStartMessage2.setWebrtcusername(username); webrtcStartMessage2.setWebrtcusername(username);
webrtcStartMessage2.setWebrtcpassword(password); webrtcStartMessage2.setWebrtcpassword(password);
} }
@ -444,10 +455,9 @@ export class SocketManager {
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
//if (!otherUser.socket.disconnecting) { //if (!otherUser.socket.disconnecting) {
otherUser.socket.write(serverToClientMessage2); otherUser.socket.write(serverToClientMessage2);
//console.log('Sending webrtcstart to '+otherUser.socket.userId) //console.log('Sending webrtcstart to '+otherUser.socket.userId)
//} //}
} }
} }
@ -456,17 +466,17 @@ export class SocketManager {
* and the Coturn server. * and the Coturn server.
* The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey` * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
*/ */
private getTURNCredentials(name: string, secret: string): {username: string, password: string} { private getTURNCredentials(name: string, secret: string): { username: string; password: string } {
const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours
const username = [unixTimeStamp, name].join(':'); const username = [unixTimeStamp, name].join(":");
const hmac = crypto.createHmac('sha1', secret); const hmac = crypto.createHmac("sha1", secret);
hmac.setEncoding('base64'); hmac.setEncoding("base64");
hmac.write(username); hmac.write(username);
hmac.end(); hmac.end();
const password = hmac.read(); const password = hmac.read();
return { return {
username: username, username: username,
password: password password: password,
}; };
} }
@ -489,10 +499,9 @@ export class SocketManager {
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
//if (!otherUser.socket.disconnecting) { //if (!otherUser.socket.disconnecting) {
otherUser.socket.write(serverToClientMessage1); otherUser.socket.write(serverToClientMessage1);
//} //}
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
webrtcDisconnectMessage2.setUserid(otherUser.id); webrtcDisconnectMessage2.setUserid(otherUser.id);
@ -500,7 +509,7 @@ export class SocketManager {
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
//if (!user.socket.disconnecting) { //if (!user.socket.disconnecting) {
user.socket.write(serverToClientMessage2); user.socket.write(serverToClientMessage2);
//} //}
} }
} }
@ -517,40 +526,41 @@ export class SocketManager {
console.error('An error occurred on "emitPlayGlobalMessage" event'); console.error('An error occurred on "emitPlayGlobalMessage" event');
console.error(e); console.error(e);
} }
} }
public getWorlds(): Map<string, GameRoom> { public getWorlds(): Map<string, GameRoom> {
return this.rooms; return this.rooms;
} }
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) { public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
const room = queryJitsiJwtMessage.getJitsiroom(); const room = queryJitsiJwtMessage.getJitsiroom();
const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead. const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead.
if (SECRET_JITSI_KEY === '') { if (SECRET_JITSI_KEY === "") {
throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.");
} }
// Let's see if the current client has // Let's see if the current client has
const isAdmin = user.tags.includes(tag); const isAdmin = user.tags.includes(tag);
const jwt = Jwt.sign({ const jwt = Jwt.sign(
"aud": "jitsi", {
"iss": JITSI_ISS, aud: "jitsi",
"sub": JITSI_URL, iss: JITSI_ISS,
"room": room, sub: JITSI_URL,
"moderator": isAdmin room: room,
}, SECRET_JITSI_KEY, { moderator: isAdmin,
expiresIn: '1d', },
algorithm: "HS256", SECRET_JITSI_KEY,
header: {
{ expiresIn: "1d",
"alg": "HS256", algorithm: "HS256",
"typ": "JWT" header: {
} alg: "HS256",
}); typ: "JWT",
},
}
);
const sendJitsiJwtMessage = new SendJitsiJwtMessage(); const sendJitsiJwtMessage = new SendJitsiJwtMessage();
sendJitsiJwtMessage.setJitsiroom(room); sendJitsiJwtMessage.setJitsiroom(room);
@ -562,7 +572,7 @@ export class SocketManager {
user.socket.write(serverToClientMessage); user.socket.write(serverToClientMessage);
} }
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){ public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(sendUserMessageToSend.getMessage()); sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
sendUserMessage.setType(sendUserMessageToSend.getType()); sendUserMessage.setType(sendUserMessageToSend.getType());
@ -572,7 +582,7 @@ export class SocketManager {
user.socket.write(serverToClientMessage); user.socket.write(serverToClientMessage);
} }
public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){ public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) {
const banUserMessage = new BanUserMessage(); const banUserMessage = new BanUserMessage();
banUserMessage.setMessage(banUserMessageToSend.getMessage()); banUserMessage.setMessage(banUserMessageToSend.getMessage());
banUserMessage.setType(banUserMessageToSend.getType()); banUserMessage.setType(banUserMessageToSend.getType());
@ -592,7 +602,7 @@ export class SocketManager {
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void { public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
console.error("In addZoneListener, could not find room with id '" + roomId + "'"); console.error("In addZoneListener, could not find room with id '" + roomId + "'");
return; return;
} }
@ -636,7 +646,7 @@ export class SocketManager {
removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) { removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
console.error("In removeZoneListener, could not find room with id '" + roomId + "'"); console.error("In removeZoneListener, could not find room with id '" + roomId + "'");
return; return;
} }
@ -651,7 +661,7 @@ export class SocketManager {
return room; return room;
} }
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.roomId);
@ -663,19 +673,27 @@ export class SocketManager {
public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void { public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
console.error("In sendAdminMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); console.error(
"In sendAdminMessage, could not find room with id '" +
roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
);
return; return;
} }
const recipient = room.getUserByUuid(recipientUuid); const recipient = room.getUserByUuid(recipientUuid);
if (recipient === undefined) { if (recipient === undefined) {
console.error("In sendAdminMessage, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); console.error(
"In sendAdminMessage, could not find user with id '" +
recipientUuid +
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
);
return; return;
} }
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message); sendUserMessage.setMessage(message);
sendUserMessage.setType('ban'); //todo: is the type correct? sendUserMessage.setType("ban"); //todo: is the type correct?
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSendusermessage(sendUserMessage); serverToClientMessage.setSendusermessage(sendUserMessage);
@ -686,13 +704,21 @@ export class SocketManager {
public banUser(roomId: string, recipientUuid: string, message: string): void { public banUser(roomId: string, recipientUuid: string, message: string): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
console.error("In banUser, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); console.error(
"In banUser, could not find room with id '" +
roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
);
return; return;
} }
const recipient = room.getUserByUuid(recipientUuid); const recipient = room.getUserByUuid(recipientUuid);
if (recipient === undefined) { if (recipient === undefined) {
console.error("In banUser, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); console.error(
"In banUser, could not find user with id '" +
recipientUuid +
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
);
return; return;
} }
@ -701,7 +727,7 @@ export class SocketManager {
const banUserMessage = new BanUserMessage(); const banUserMessage = new BanUserMessage();
banUserMessage.setMessage(message); banUserMessage.setMessage(message);
banUserMessage.setType('banned'); banUserMessage.setType("banned");
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setBanusermessage(banUserMessage); serverToClientMessage.setBanusermessage(banUserMessage);
@ -711,19 +737,22 @@ export class SocketManager {
recipient.socket.end(); recipient.socket.end();
} }
sendAdminRoomMessage(roomId: string, message: string) { sendAdminRoomMessage(roomId: string, message: string) {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
//todo: this should cause the http call to return a 500 //todo: this should cause the http call to return a 500
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); console.error(
"In sendAdminRoomMessage, could not find room with id '" +
roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
);
return; return;
} }
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message); sendUserMessage.setMessage(message);
sendUserMessage.setType('message'); sendUserMessage.setType("message");
const clientMessage = new ServerToClientMessage(); const clientMessage = new ServerToClientMessage();
clientMessage.setSendusermessage(sendUserMessage); clientMessage.setSendusermessage(sendUserMessage);
@ -732,14 +761,18 @@ export class SocketManager {
}); });
} }
dispatchWorlFullWarning(roomId: string,): void { dispatchWorlFullWarning(roomId: string): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
//todo: this should cause the http call to return a 500 //todo: this should cause the http call to return a 500
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); console.error(
"In sendAdminRoomMessage, could not find room with id '" +
roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
);
return; return;
} }
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const worldFullMessage = new WorldFullWarningMessage(); const worldFullMessage = new WorldFullWarningMessage();
@ -750,17 +783,17 @@ export class SocketManager {
}); });
} }
dispatchRoomRefresh(roomId: string,): void { dispatchRoomRefresh(roomId: string): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
return; return;
} }
const versionNumber = room.incrementVersion(); const versionNumber = room.incrementVersion();
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const worldFullMessage = new RefreshRoomMessage(); const worldFullMessage = new RefreshRoomMessage();
worldFullMessage.setRoomid(roomId) worldFullMessage.setRoomid(roomId);
worldFullMessage.setVersionnumber(versionNumber) worldFullMessage.setVersionnumber(versionNumber);
const clientMessage = new ServerToClientMessage(); const clientMessage = new ServerToClientMessage();
clientMessage.setRefreshroommessage(worldFullMessage); clientMessage.setRefreshroommessage(worldFullMessage);

View file

@ -117,6 +117,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/strip-bom@^3.0.0": "@types/strip-bom@^3.0.0":
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
@ -197,6 +202,14 @@ acorn@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c"
integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
dependencies:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv@^6.10.0, ajv@^6.10.2: ajv@^6.10.0, ajv@^6.10.2:
version "6.12.5" version "6.12.5"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
@ -207,6 +220,11 @@ ajv@^6.10.0, ajv@^6.10.2:
json-schema-traverse "^0.4.1" json-schema-traverse "^0.4.1"
uri-js "^4.2.2" uri-js "^4.2.2"
ansi-colors@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
ansi-escapes@^4.2.1: ansi-escapes@^4.2.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
@ -214,6 +232,13 @@ ansi-escapes@^4.2.1:
dependencies: dependencies:
type-fest "^0.11.0" type-fest "^0.11.0"
ansi-escapes@^4.3.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
dependencies:
type-fest "^0.21.3"
ansi-regex@^2.0.0: ansi-regex@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@ -241,6 +266,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
dependencies: dependencies:
color-convert "^1.9.0" color-convert "^1.9.0"
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
ansi-styles@^4.1.0: ansi-styles@^4.1.0:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
@ -325,6 +357,11 @@ astral-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
atob@^2.1.2: atob@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
@ -389,7 +426,7 @@ braces@^2.3.1:
split-string "^3.0.2" split-string "^3.0.2"
to-regex "^3.0.1" to-regex "^3.0.1"
braces@~3.0.2: braces@^3.0.1, braces@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@ -475,6 +512,14 @@ chalk@^4.1.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chalk@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chardet@^0.7.0: chardet@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@ -515,6 +560,11 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cli-cursor@^3.1.0: cli-cursor@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@ -522,6 +572,14 @@ cli-cursor@^3.1.0:
dependencies: dependencies:
restore-cursor "^3.1.0" restore-cursor "^3.1.0"
cli-truncate@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
dependencies:
slice-ansi "^3.0.0"
string-width "^4.2.0"
cli-width@^3.0.0: cli-width@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
@ -573,11 +631,21 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
colour@~0.7.1: colour@~0.7.1:
version "0.7.1" version "0.7.1"
resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778"
integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=
commander@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
component-emitter@^1.2.1: component-emitter@^1.2.1:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@ -603,6 +671,17 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
cosmiconfig@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3"
integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==
dependencies:
"@types/parse-json" "^4.0.0"
import-fresh "^3.2.1"
parse-json "^5.0.0"
path-type "^4.0.0"
yaml "^1.10.0"
cross-spawn@^6.0.5: cross-spawn@^6.0.5:
version "6.0.5" version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -614,6 +693,15 @@ cross-spawn@^6.0.5:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
currently-unhandled@^0.4.1: currently-unhandled@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@ -667,6 +755,11 @@ decode-uri-component@^0.2.0:
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
dedent@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
deep-extend@^0.6.0: deep-extend@^0.6.0:
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
@ -752,7 +845,14 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
error-ex@^1.2.0: enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
dependencies:
ansi-colors "^4.1.1"
error-ex@^1.2.0, error-ex@^1.3.1:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
@ -877,6 +977,21 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
dependencies:
cross-spawn "^7.0.3"
get-stream "^6.0.0"
human-signals "^2.1.0"
is-stream "^2.0.0"
merge-stream "^2.0.0"
npm-run-path "^4.0.1"
onetime "^5.1.2"
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
expand-brackets@^2.1.4: expand-brackets@^2.1.4:
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@ -1066,11 +1181,21 @@ generic-type-guard@^3.2.0:
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1" resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1"
integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ== integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ==
get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==
get-stdin@^4.0.1: get-stdin@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
get-value@^2.0.3, get-value@^2.0.6: get-value@^2.0.3, get-value@^2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@ -1193,6 +1318,11 @@ http-status-codes@*:
resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798"
integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg==
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
iconv-lite@^0.4.24, iconv-lite@^0.4.4: iconv-lite@^0.4.24, iconv-lite@^0.4.4:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -1220,6 +1350,14 @@ import-fresh@^3.0.0:
parent-module "^1.0.0" parent-module "^1.0.0"
resolve-from "^4.0.0" resolve-from "^4.0.0"
import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
imurmurhash@^0.1.4: imurmurhash@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@ -1232,6 +1370,11 @@ indent-string@^2.1.0:
dependencies: dependencies:
repeating "^2.0.0" repeating "^2.0.0"
indent-string@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
inflight@^1.0.4: inflight@^1.0.4:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@ -1402,6 +1545,11 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
is-plain-object@^2.0.3, is-plain-object@^2.0.4: is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@ -1409,6 +1557,21 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies: dependencies:
isobject "^3.0.1" isobject "^3.0.1"
is-regexp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk=
is-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
is-unicode-supported@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
is-utf8@^0.2.0: is-utf8@^0.2.0:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
@ -1467,6 +1630,11 @@ js-yaml@^3.13.1:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json-schema-traverse@^0.4.1: json-schema-traverse@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@ -1549,6 +1717,45 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2" prelude-ls "~1.1.2"
type-check "~0.3.2" type-check "~0.3.2"
lines-and-columns@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
lint-staged@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712"
integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw==
dependencies:
chalk "^4.1.1"
cli-truncate "^2.1.0"
commander "^7.2.0"
cosmiconfig "^7.0.0"
debug "^4.3.1"
dedent "^0.7.0"
enquirer "^2.3.6"
execa "^5.0.0"
listr2 "^3.8.2"
log-symbols "^4.1.0"
micromatch "^4.0.4"
normalize-path "^3.0.0"
please-upgrade-node "^3.2.0"
string-argv "0.3.1"
stringify-object "^3.3.0"
listr2@^3.8.2:
version "3.10.0"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f"
integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw==
dependencies:
cli-truncate "^2.1.0"
colorette "^1.2.2"
log-update "^4.0.0"
p-map "^4.0.0"
rxjs "^6.6.7"
through "^2.3.8"
wrap-ansi "^7.0.0"
load-json-file@^1.0.0: load-json-file@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@ -1610,6 +1817,24 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
dependencies:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
log-update@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
dependencies:
ansi-escapes "^4.3.0"
cli-cursor "^3.1.0"
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
long@~3: long@~3:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b"
@ -1661,6 +1886,11 @@ meow@^3.3.0:
redent "^1.0.0" redent "^1.0.0"
trim-newlines "^1.0.0" trim-newlines "^1.0.0"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
merge2@^1.2.3: merge2@^1.2.3:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@ -1685,6 +1915,14 @@ micromatch@^3.1.10:
snapdragon "^0.8.1" snapdragon "^0.8.1"
to-regex "^3.0.2" to-regex "^3.0.2"
micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
dependencies:
braces "^3.0.1"
picomatch "^2.2.3"
mimic-fn@^2.1.0: mimic-fn@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -1853,6 +2091,13 @@ npm-packlist@^1.1.6:
npm-bundled "^1.0.1" npm-bundled "^1.0.1"
npm-normalize-package-bin "^1.0.1" npm-normalize-package-bin "^1.0.1"
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies:
path-key "^3.0.0"
npmlog@^4.0.2: npmlog@^4.0.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
@ -1903,7 +2148,7 @@ once@^1.3.0:
dependencies: dependencies:
wrappy "1" wrappy "1"
onetime@^5.1.0: onetime@^5.1.0, onetime@^5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
@ -1952,6 +2197,13 @@ osenv@^0.1.4:
os-homedir "^1.0.0" os-homedir "^1.0.0"
os-tmpdir "^1.0.0" os-tmpdir "^1.0.0"
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
dependencies:
aggregate-error "^3.0.0"
parent-module@^1.0.0: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@ -1966,6 +2218,16 @@ parse-json@^2.2.0:
dependencies: dependencies:
error-ex "^1.2.0" error-ex "^1.2.0"
parse-json@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
dependencies:
"@babel/code-frame" "^7.0.0"
error-ex "^1.3.1"
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
pascalcase@^0.1.1: pascalcase@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
@ -1993,6 +2255,11 @@ path-key@^2.0.1:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.6: path-parse@^1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
@ -2007,11 +2274,21 @@ path-type@^1.0.0:
pify "^2.0.0" pify "^2.0.0"
pinkie-promise "^2.0.0" pinkie-promise "^2.0.0"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picomatch@^2.0.4, picomatch@^2.2.1: picomatch@^2.0.4, picomatch@^2.2.1:
version "2.2.2" version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
picomatch@^2.2.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
pify@^2.0.0: pify@^2.0.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -2029,6 +2306,13 @@ pinkie@^2.0.0:
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==
dependencies:
semver-compare "^1.0.0"
posix-character-classes@^0.1.0: posix-character-classes@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -2039,6 +2323,11 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
prettier@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==
process-nextick-args@~2.0.0: process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -2226,6 +2515,13 @@ rxjs@^6.6.0:
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
rxjs@^6.6.7:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
dependencies:
tslib "^1.9.0"
safe-buffer@^5.0.1, safe-buffer@^5.1.2: safe-buffer@^5.0.1, safe-buffer@^5.1.2:
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@ -2253,6 +2549,11 @@ sax@^1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -2290,12 +2591,24 @@ shebang-command@^1.2.0:
dependencies: dependencies:
shebang-regex "^1.0.0" shebang-regex "^1.0.0"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^1.0.0: shebang-regex@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
signal-exit@^3.0.0, signal-exit@^3.0.2: shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
version "3.0.3" version "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==
@ -2309,6 +2622,24 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0" astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
slice-ansi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
dependencies:
ansi-styles "^4.0.0"
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
dependencies:
ansi-styles "^4.0.0"
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
snapdragon-node@^2.0.1: snapdragon-node@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -2434,6 +2765,11 @@ strict-uri-encode@^2.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
string-argv@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
string-width@^1.0.1: string-width@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@ -2469,6 +2805,15 @@ string-width@^4.1.0:
is-fullwidth-code-point "^3.0.0" is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
string-width@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string_decoder@~1.1.1: string_decoder@~1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@ -2476,6 +2821,15 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
stringify-object@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629"
integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==
dependencies:
get-own-enumerable-property-symbols "^3.0.0"
is-obj "^1.0.1"
is-regexp "^1.0.0"
strip-ansi@^3.0.0, strip-ansi@^3.0.1: strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@ -2516,6 +2870,11 @@ strip-bom@^3.0.0:
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-indent@^1.0.1: strip-indent@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
@ -2587,7 +2946,7 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
through@^2.3.6: through@^2.3.6, through@^2.3.8:
version "2.3.8" version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@ -2703,6 +3062,11 @@ type-fest@^0.11.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
type-fest@^0.21.3:
version "0.21.3"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
type-fest@^0.8.1: type-fest@^0.8.1:
version "0.8.1" version "0.8.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
@ -2790,6 +3154,13 @@ which@^1.2.9:
dependencies: dependencies:
isexe "^2.0.0" isexe "^2.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
wide-align@^1.1.0: wide-align@^1.1.0:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
@ -2815,6 +3186,24 @@ wrap-ansi@^2.0.0:
string-width "^1.0.1" string-width "^1.0.1"
strip-ansi "^3.0.1" strip-ansi "^3.0.1"
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@ -2842,6 +3231,11 @@ yallist@^3.0.0, yallist@^3.0.3:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yaml@^1.10.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yargs@^3.10.0: yargs@^3.10.0:
version "3.32.0" version "3.32.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"

33
docs/maps/animations.md Normal file
View file

@ -0,0 +1,33 @@
{.section-title.accent.text-primary}
# Animating WorkAdventure maps
A tile can run an animation in loops, for example to render water or blinking lights. Each animation frame is a single
32x32 tile. To create an animation, edit the tileset in Tiled and click on the tile to animate (or pick a free tile to
not overwrite existing ones) and click on the animation editor:
<div class="px-5 card rounded d-inline-block">
<img class="document-img" src="https://workadventu.re/img/docs/anims/camera.png" alt="" />
</div>
You can now add all tiles that should be part of the animation via drag and drop to the "playlist" and adjust the frame duration:
<div>
<figure class="figure">
<img class="figure-img img-fluid rounded" src="https://workadventu.re/img/docs/anims/animation_editor.png" alt="" />
<figcaption class="figure-caption">The tile animation editor</figcaption>
</figure>
</div>
You can preview animations directly in Tiled, using the "Show tile animations" option:
<div>
<figure class="figure">
<img class="figure-img img-fluid rounded" src="https://workadventu.re/img/docs/anims/settings_show_animations.png" alt="" />
<figcaption class="figure-caption">The Show Tile Animations option</figcaption>
</figure>
</div>
{.alert.alert-info}
**Tip:** The engine does tile-updates every 100ms, animations with a shorter frame duration will most likely not look that good or may even do not work.

21
docs/maps/api-player.md Normal file
View file

@ -0,0 +1,21 @@
{.section-title.accent.text-primary}
# API Player functions Reference
### Listen to player movement
```
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
```
Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction.
The event has the following attributes :
* **moving (boolean):** **true** when the current player is moving, **false** otherwise.
* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving.
* **x (number):** coordinate X of the current player.
* **y (number):** coordinate Y of the current player.
**callback:** the function that will be called when the current player is moving. It contains the event.
Example :
```javascript
WA.player.onPlayerMove(console.log);
```

View file

@ -4,8 +4,9 @@
- [Navigation functions](api-nav.md) - [Navigation functions](api-nav.md)
- [Chat functions](api-chat.md) - [Chat functions](api-chat.md)
- [Room functions](api-room.md) - [Room functions](api-room.md)
- [Player functions](api-player.md)
- [UI functions](api-ui.md) - [UI functions](api-ui.md)
- [Sound functions](api-sound.md) - [Sound functions](api-sound.md)
- [Controls functions](api-controls.md) - [Controls functions](api-controls.md)
- [List of deprecated functions](api-deprecated.md) - [List of deprecated functions](api-deprecated.md)

View file

@ -1,6 +1,22 @@
{.section-title.accent.text-primary} {.section-title.accent.text-primary}
# API Room functions Reference # API Room functions Reference
### Working with group layers
If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together.
Example :
<div class="row">
<div class="col">
<img src="https://workadventu.re/img/docs/groupLayer.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
The name of the layers of this map are :
* `entries/start`
* `bottom/ground/under`
* `bottom/build/carpet`
* `wall`
### Detecting when the user enters/leaves a zone ### Detecting when the user enters/leaves a zone
``` ```
@ -31,3 +47,68 @@ WA.room.onLeaveZone('myZone', () => {
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot'); WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
}) })
``` ```
### Show / Hide a layer
```
WA.room.showLayer(layerName : string): void
WA.room.hideLayer(layerName : string) : void
```
These 2 methods can be used to show and hide a layer.
Example :
```javascript
WA.room.showLayer('bottom');
//...
WA.room.hideLayer('bottom');
```
### Set/Create properties in a layer
```
WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void;
```
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
Example :
```javascript
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
```
### Getting information on the current room
```
WA.room.getCurrentRoom(): Promise<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);
}
})
```

View file

@ -1,5 +1,5 @@
{.section-title.accent.text-primary} {.section-title.accent.text-primary}
# API Room functions Reference # API UI functions Reference
### Opening a popup ### Opening a popup
@ -65,3 +65,25 @@ WA.room.onLeaveZone('myZone', () => {
helloWorldPopup.close(); helloWorldPopup.close();
}); });
``` ```
### Add custom menu
```typescript
WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void
```
Add a custom menu item containing the text `commandDescriptor` in the main menu. A click on the menu will trigger the `callback`.
Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script.
Example:
```javascript
WA.ui.registerMenuCommand("test", () => {
WA.chat.sendChatMessage("test clicked", "menu cmd")
})
```
<div class="col">
<img src="https://workadventu.re/img/docs/menu-command.png" class="figure-img img-fluid rounded" alt="" />
</div>

View file

@ -101,7 +101,7 @@ You can now start by testing this with a simple message sent to the chat.
```html ```html
... ...
<script> <script>
WA.sendChatMessage('Hello world', 'Mr Robot'); WA.chat.sendChatMessage('Hello world', 'Mr Robot');
</script> </script>
... ...
``` ```

92
docs/maps/wa-maps.md Normal file
View file

@ -0,0 +1,92 @@
{.section-title.accent.text-primary}
# About WorkAdventure maps
A WorkAdventure map is a map in "JSON" format generated by [Tiled](https://www.mapeditor.org/).
## Tiles
A map is made of "tiles" (we can also call them "sprites"). In WorkAdventure, the tiles are small images of 32x32 pixels.
Tiles may have transparent parts. Many tiles can be stored in a single PNG file. We call this file a "tileset".
There are many tilesets available on the internet. Some examples of websites offering awesome tiles:
* [itch.io](https://itch.io/)
* [opengameart.org](https://opengameart.org/)
* [deviantart.com](https://www.deviantart.com/)
Keep in mind the size of tiles and do not forget to check the license of the tileset you are using!
## How to design "pixel" tiles
You can design your own tiles as well as change existing tiles, this is usually referred to as "pixeling". You can start drawing your own tiles with [Piskel](https://www.piskelapp.com/). It is easy to use and well targeted at "pixeling". If you are getting serious about pixeling, the awesome folks at the Chaos Computer Club recommend the use of the editor [Krita](https://krita.org/). There are plenty of other editors as well.
If you are using Krita:
* Please double check that your tiles are 32x32 pixels in size. You can enable a grid under view -> show grid and under settings -> dockers -> grid you can select the grid size.
* Use transparency if you have to model transitions between different materials. This is more flexible and saves you time by not modeling every transition.
* You can follow the Pixel-Art Workshop by blinry: [media.ccc.de/v/34C3-jugend-hackt-1016-pixel_art_workshop](https://media.ccc.de/v/34C3-jugend-hackt-1016-pixel_art_workshop)
## WorkAdventure Map Rules
In order to design a map that will be readable by WorkAdventure, you will have to respect some constraints.
In particular, you will need to:
* set a start position for the players
* configure the "floor layer" (so that WorkAdventure can correctly display characters above the floor, but under the ceiling)
* eventually, you can place exits that link to other maps
A few things to notice:
* your map can have as many layers as you want
* your map MUST contain a layer named "floorLayer" of type "objectgroup" that represents the layer on which characters will be drawn. Every layer above the "floorLayer" will be displayed on top of the characters.
* the tilesets in your map MUST be embedded. You cannot refer to an external typeset in a TSX file. Click the "embed tileset" button in the tileset tab to embed tileset data.
* your map MUST be exported in JSON format. You need to use a recent version of Tiled to get JSON format export (1.3+)
* WorkAdventure doesn't support object layers and will ignore them
* If you are starting from a blank map, your map MUST be orthogonal and tiles size should be 32x32.
<div>
<figure class="figure">
<img src="https://workadventu.re/img/docs/tiled_screenshot_1.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">"floorLayer" is compulsory</figcaption>
</figure>
</div>
## Building walls and "collidable" areas
By default, the characters can traverse any tiles. If you want to prevent your characeter from going through a tile (like a wall or a desktop), you must make this tile "collidable". You can do this by settings the `collides` property on a given tile.
To make a tile "collidable", you should:
1. select the relevant tileset and switch to "edit" mode:
![](https://workadventu.re/img/docs/collides-1.png){.document-img}
2. right click on a tile of the tileset to select it:
![](https://workadventu.re/img/docs/collides-2.png){.document-img}
3. on the left pane in the custom properties section, right click and select "Add properties":
![](https://workadventu.re/img/docs/collides-3.png){.document-img}
Please add a `collides` property. The type of the property must be **bool**.
4. finally, check the checkbox for the `collides` property:
![](https://workadventu.re/img/docs/collides-4.png){.document-img}
Repeat for every tile that should be "collidable".
## Adding behaviour with properties
In the next sections, you will see how you can add behaviour on your map by adding "properties".
You can add properties for a variety of features: putting exits, opening websites, meeting rooms, silent zones, etc...
You can add properties either on individual tiles of a tileset OR on a complete layer.
If you put a property on a layer, it will be triggered if your Woka walks on any tile of the layer.
The exception is the "collides" property that can only be set on tiles, but not on a complete layer.

6
front/.gitignore vendored
View file

@ -1,5 +1,9 @@
/node_modules/ /node_modules/
/dist/bundle.js /dist/*.js
/dist/*.js.map
/dist/*.js.LICENSE.txt
/dist/main.*.css
/dist/main.*.css.map
/dist/tests/ /dist/tests/
/yarn-error.log /yarn-error.log
/dist/webpack.config.js /dist/webpack.config.js

1
front/.prettierignore Normal file
View file

@ -0,0 +1 @@
src/Messages/generated

4
front/.prettierrc.json Normal file
View file

@ -0,0 +1,4 @@
{
"printWidth": 120,
"tabWidth": 4
}

8393
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -18,9 +18,11 @@
"fork-ts-checker-webpack-plugin": "^6.2.9", "fork-ts-checker-webpack-plugin": "^6.2.9",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0", "jasmine": "^3.5.0",
"lint-staged": "^11.0.0",
"mini-css-extract-plugin": "^1.6.0", "mini-css-extract-plugin": "^1.6.0",
"node-polyfill-webpack-plugin": "^1.1.2", "node-polyfill-webpack-plugin": "^1.1.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.3.1",
"sass": "^1.32.12", "sass": "^1.32.12",
"sass-loader": "^11.1.0", "sass-loader": "^11.1.0",
"svelte": "^3.38.2", "svelte": "^3.38.2",
@ -44,6 +46,7 @@
"generic-type-guard": "^3.2.0", "generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"phaser": "^3.54.0", "phaser": "^3.54.0",
"phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254",
"phaser3-rex-plugins": "^1.1.42", "phaser3-rex-plugins": "^1.1.42",
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "1.3.6", "quill": "1.3.6",
@ -53,13 +56,22 @@
"standardized-audio-context": "^25.2.4" "standardized-audio-context": "^25.2.4"
}, },
"scripts": { "scripts": {
"start": "run-p serve svelte-check-watch", "start": "run-p templater serve svelte-check-watch",
"templater": "cross-env ./templater.sh",
"serve": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "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",
"test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts", "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",
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch", "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"" "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
},
"lint-staged": {
"*.ts": [
"prettier --write"
]
} }
} }

View file

@ -1,397 +0,0 @@
import {HtmlUtils} from "../WebRtc/HtmlUtils";
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import type {RoomConnection} from "../Connexion/RoomConnection";
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export const CLASS_CONSOLE_MESSAGE = 'main-console';
export const INPUT_CONSOLE_MESSAGE = 'input-send-text';
export const UPLOAD_CONSOLE_MESSAGE = 'input-upload-music';
export const INPUT_TYPE_CONSOLE = 'input-type';
export const VIDEO_QUALITY_SELECT = 'select-video-quality';
export const AUDIO_TYPE = AdminMessageEventTypes.audio;
export const MESSAGE_TYPE = AdminMessageEventTypes.admin;
interface EventTargetFiles extends EventTarget {
files: Array<File>;
}
/**
* @deprecated
*/
export class ConsoleGlobalMessageManager {
private readonly divMainConsole: HTMLDivElement;
private readonly divMessageConsole: HTMLDivElement;
//private readonly divSettingConsole: HTMLDivElement;
private readonly buttonMainConsole: HTMLDivElement;
private readonly buttonSendMainConsole: HTMLImageElement;
//private readonly buttonAdminMainConsole: HTMLImageElement;
//private readonly buttonSettingsMainConsole: HTMLImageElement;
private activeConsole: boolean = false;
private activeMessage: boolean = false;
private activeSetting: boolean = false;
private userInputManager!: UserInputManager;
private static cssLoaded: boolean = false;
constructor(private Connection: RoomConnection, userInputManager : UserInputManager, private isAdmin: Boolean) {
this.buttonMainConsole = document.createElement('div');
this.buttonMainConsole.classList.add('console');
this.buttonMainConsole.hidden = true;
this.divMainConsole = document.createElement('div');
this.divMainConsole.className = CLASS_CONSOLE_MESSAGE;
this.divMessageConsole = document.createElement('div');
this.divMessageConsole.className = 'message';
//this.divSettingConsole = document.createElement('div');
//this.divSettingConsole.className = 'setting';
this.buttonSendMainConsole = document.createElement('img');
this.buttonSendMainConsole.id = 'btn-send-message';
//this.buttonSettingsMainConsole = document.createElement('img');
//this.buttonAdminMainConsole = document.createElement('img');
this.userInputManager = userInputManager;
this.initialise();
}
initialise() {
for (const elem of document.getElementsByClassName(CLASS_CONSOLE_MESSAGE)) {
elem.remove();
}
const typeConsole = document.createElement('input');
typeConsole.id = INPUT_TYPE_CONSOLE;
typeConsole.value = MESSAGE_TYPE;
typeConsole.type = 'hidden';
const menu = document.createElement('div');
menu.classList.add('menu')
const textMessage = document.createElement('span');
textMessage.innerText = "Message";
textMessage.classList.add('active');
textMessage.addEventListener('click', () => {
textMessage.classList.add('active');
const messageSection = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(this.getSectionId(INPUT_CONSOLE_MESSAGE));
messageSection.classList.add('active');
textAudio.classList.remove('active');
const audioSection = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(this.getSectionId(UPLOAD_CONSOLE_MESSAGE));
audioSection.classList.remove('active');
typeConsole.value = MESSAGE_TYPE;
});
menu.appendChild(textMessage);
const textAudio = document.createElement('span');
textAudio.innerText = "Audio";
textAudio.addEventListener('click', () => {
textAudio.classList.add('active');
const audioSection = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(this.getSectionId(UPLOAD_CONSOLE_MESSAGE));
audioSection.classList.add('active');
textMessage.classList.remove('active');
const messageSection = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(this.getSectionId(INPUT_CONSOLE_MESSAGE));
messageSection.classList.remove('active');
typeConsole.value = AUDIO_TYPE;
});
menu.appendChild(textMessage);
menu.appendChild(textAudio);
this.divMessageConsole.appendChild(menu);
this.buttonSendMainConsole.src = 'resources/logos/send-yellow.svg';
this.buttonSendMainConsole.addEventListener('click', () => {
if(this.activeMessage){
this.disabledMessageConsole();
}else{
this.activeMessageConsole();
}
});
/*this.buttonAdminMainConsole.src = 'resources/logos/setting-yellow.svg';
this.buttonAdminMainConsole.addEventListener('click', () => {
window.open(ADMIN_URL, '_blank');
});*/
/*this.buttonSettingsMainConsole.src = 'resources/logos/monitor-yellow.svg';
this.buttonSettingsMainConsole.addEventListener('click', () => {
if(this.activeSetting){
this.disabledSettingConsole();
}else{
this.activeSettingConsole();
}
});*/
this.divMessageConsole.appendChild(typeConsole);
/*if(this.isAdmin) {
this.buttonMainConsole.appendChild(this.buttonSendMainConsole);
//this.buttonMainConsole.appendChild(this.buttonAdminMainConsole);
}*/
this.createTextMessagePart();
this.createUploadAudioPart();
//this.buttonMainConsole.appendChild(this.buttonSettingsMainConsole);
this.divMainConsole.appendChild(this.buttonMainConsole);
this.divMainConsole.appendChild(this.divMessageConsole);
//this.divMainConsole.appendChild(this.divSettingConsole);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(this.divMainConsole);
}
createTextMessagePart(){
const div = document.createElement('div');
div.id = INPUT_CONSOLE_MESSAGE
const buttonSend = document.createElement('button');
buttonSend.innerText = 'Send';
buttonSend.classList.add('btn');
buttonSend.addEventListener('click', (event: MouseEvent) => {
this.sendMessage();
this.disabledMessageConsole();
});
const buttonDiv = document.createElement('div');
buttonDiv.classList.add('btn-action');
buttonDiv.appendChild(buttonSend)
const section = document.createElement('section');
section.id = this.getSectionId(INPUT_CONSOLE_MESSAGE);
section.classList.add('active');
section.appendChild(div);
section.appendChild(buttonDiv);
this.divMessageConsole.appendChild(section);
(async () => {
try{
// Start loading CSS
const cssPromise = ConsoleGlobalMessageManager.loadCss();
// Import quill
const {default: Quill}:any = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
// Wait for CSS to be loaded
await cssPromise;
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
[{'header': 1}, {'header': 2}], // custom button values
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'script': 'sub'}, {'script': 'super'}], // superscript/subscript
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
[{'direction': 'rtl'}], // text direction
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}], // dropdown with defaults from theme
[{'font': []}],
[{'align': []}],
['clean'],
['link', 'image', 'video']
// remove formatting button
];
new Quill(`#${INPUT_CONSOLE_MESSAGE}`, {
theme: 'snow',
modules: {
toolbar: toolbarOptions
},
});
}catch(err){
console.error(err);
}
})();
}
createUploadAudioPart(){
const div = document.createElement('div');
div.classList.add('upload');
const label = document.createElement('label');
label.setAttribute('for', UPLOAD_CONSOLE_MESSAGE);
const img = document.createElement('img');
img.setAttribute('for', UPLOAD_CONSOLE_MESSAGE);
img.src = 'resources/logos/music-file.svg';
const input = document.createElement('input');
input.type = 'file';
input.id = UPLOAD_CONSOLE_MESSAGE
input.addEventListener('input', (e: Event) => {
if(!e.target){
return;
}
const eventTarget : EventTargetFiles = (e.target as EventTargetFiles);
if(!eventTarget || !eventTarget.files || eventTarget.files.length === 0){
return;
}
const file : File = eventTarget.files[0];
if(!file){
return;
}
try {
HtmlUtils.removeElementByIdOrFail('audi-message-filename');
}catch (err) {
console.error(err)
}
const p = document.createElement('p');
p.id = 'audi-message-filename';
p.innerText = `${file.name} : ${this.getFileSize(file.size)}`;
label.appendChild(p);
});
label.appendChild(img);
div.appendChild(label);
div.appendChild(input);
const buttonSend = document.createElement('button');
buttonSend.innerText = 'Send';
buttonSend.classList.add('btn');
buttonSend.addEventListener('click', (event: MouseEvent) => {
this.sendMessage();
this.disabledMessageConsole();
});
const buttonDiv = document.createElement('div');
buttonDiv.classList.add('btn-action');
buttonDiv.appendChild(buttonSend)
const section = document.createElement('section');
section.id = this.getSectionId(UPLOAD_CONSOLE_MESSAGE);
section.appendChild(div);
section.appendChild(buttonDiv);
this.divMessageConsole.appendChild(section);
}
private static loadCss(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (ConsoleGlobalMessageManager.cssLoaded) {
resolve();
return;
}
const fileref = document.createElement("link")
fileref.setAttribute("rel", "stylesheet")
fileref.setAttribute("type", "text/css")
fileref.setAttribute("href", "https://cdn.quilljs.com/1.3.7/quill.snow.css");
document.getElementsByTagName("head")[0].appendChild(fileref);
ConsoleGlobalMessageManager.cssLoaded = true;
fileref.onload = () => {
resolve();
}
fileref.onerror = () => {
reject();
}
});
}
sendMessage(){
const inputType = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(INPUT_TYPE_CONSOLE);
if(AUDIO_TYPE !== inputType.value && MESSAGE_TYPE !== inputType.value){
throw "Error event type";
}
if(AUDIO_TYPE === inputType.value){
return this.sendAudioMessage();
}
return this.sendTextMessage();
}
private sendTextMessage(){
const elements = document.getElementsByClassName('ql-editor');
const quillEditor = elements.item(0);
if(!quillEditor){
throw "Error get quill node";
}
const GlobalMessage : PlayGlobalMessageInterface = {
id: "1", // FIXME: use another ID?
message: quillEditor.innerHTML,
type: MESSAGE_TYPE
};
quillEditor.innerHTML = '';
this.Connection.emitGlobalMessage(GlobalMessage);
}
private async sendAudioMessage(){
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(UPLOAD_CONSOLE_MESSAGE);
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if(!selectedFile){
throw 'no file selected';
}
const fd = new FormData();
fd.append('file', selectedFile);
const res = await this.Connection.uploadAudio(fd);
const GlobalMessage : PlayGlobalMessageInterface = {
id: (res as {id: string}).id,
message: (res as {path: string}).path,
type: AUDIO_TYPE
};
inputAudio.value = '';
try {
HtmlUtils.removeElementByIdOrFail('audi-message-filename');
}catch (err) {
console.error(err);
}
this.Connection.emitGlobalMessage(GlobalMessage);
}
active(){
this.userInputManager.disableControls();
this.divMainConsole.style.top = '0';
this.activeConsole = true;
}
disabled(){
this.userInputManager.initKeyBoardEvent();
this.activeConsole = false;
this.divMainConsole.style.top = '-80%';
}
activeMessageConsole(){
if(!this.isAdmin){
throw "User is not admin";
}
if(this.activeMessage){
this.disabledMessageConsole();
return;
}
this.activeMessage = true;
this.active();
this.divMessageConsole.classList.add('active');
this.buttonMainConsole.hidden = false;
this.buttonSendMainConsole.classList.add('active');
//if button not
try{
HtmlUtils.getElementByIdOrFail('btn-send-message');
}catch (e) {
this.buttonMainConsole.appendChild(this.buttonSendMainConsole);
}
}
disabledMessageConsole(){
this.activeMessage = false;
this.disabled();
this.buttonMainConsole.hidden = true;
this.divMessageConsole.classList.remove('active');
this.buttonSendMainConsole.classList.remove('active');
}
private getSectionId(id: string) : string {
return `section-${id}`;
}
private getFileSize(number: number) :string {
if (number < 1024) {
return number + 'bytes';
} else if (number >= 1024 && number < 1048576) {
return (number / 1024).toFixed(1) + 'KB';
} else if (number >= 1048576) {
return (number / 1048576).toFixed(1) + 'MB';
}else{
return '';
}
}
}

View file

@ -1,10 +1,10 @@
import {HtmlUtils} from "./../WebRtc/HtmlUtils"; import {HtmlUtils} from "./../WebRtc/HtmlUtils";
import {AUDIO_TYPE, MESSAGE_TYPE} from "./ConsoleGlobalMessageManager";
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection"; import type {RoomConnection} from "../Connexion/RoomConnection";
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels"; import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {soundPlayingStore} from "../Stores/SoundPlayingStore"; import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import {soundManager} from "../Phaser/Game/SoundManager"; import {soundManager} from "../Phaser/Game/SoundManager";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export class GlobalMessageManager { export class GlobalMessageManager {
@ -36,11 +36,11 @@ export class GlobalMessageManager {
previousMessage.remove(); previousMessage.remove();
} }
if(AUDIO_TYPE === message.type){ if(AdminMessageEventTypes.audio === message.type){
this.playAudioMessage(message.id, message.message); this.playAudioMessage(message.id, message.message);
} }
if(MESSAGE_TYPE === message.type){ if(AdminMessageEventTypes.admin === message.type){
this.playTextMessage(message.id, message.message); this.playTextMessage(message.id, message.message);
} }
} }

View file

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

View file

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

View file

@ -0,0 +1,19 @@
import * as tg from "generic-type-guard";
export const isHasPlayerMovedEvent =
new tg.IsInterface().withProperties({
direction: tg.isElementOf('right', 'left', 'up', 'down'),
moving: tg.isBoolean,
x: tg.isNumber,
y: tg.isNumber
}).get();
/**
* 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 HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void

View file

@ -1,5 +1,5 @@
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';
@ -10,8 +10,14 @@ 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 { LoadSoundEvent} from "./LoadSoundEvent"; import type { DataLayerEvent } from "./DataLayerEvent";
import type {PlaySoundEvent} from "./PlaySoundEvent"; import type { LayerEvent } from './LayerEvent';
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent';
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
@ -33,9 +39,16 @@ export type IframeEventMap = {
restorePlayerControls: null restorePlayerControls: null
displayBubble: null displayBubble: null
removeBubble: null removeBubble: null
onPlayerMove: undefined
showLayer: LayerEvent
hideLayer: LayerEvent
setProperty: SetPropertyEvent
getDataLayer: undefined
loadSound: LoadSoundEvent loadSound: LoadSoundEvent
playSound: PlaySoundEvent playSound: PlaySoundEvent
stopSound: null, stopSound: null,
getState: undefined,
registerMenuCommand: MenuItemRegisterEvent
} }
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;
@ -51,7 +64,10 @@ export interface IframeResponseEventMap {
enterEvent: EnterLeaveEvent enterEvent: EnterLeaveEvent
leaveEvent: EnterLeaveEvent leaveEvent: EnterLeaveEvent
buttonClickedEvent: ButtonClickedEvent buttonClickedEvent: ButtonClickedEvent
// gameState: GameStateEvent gameState: GameStateEvent
hasPlayerMoved: HasPlayerMovedEvent
dataLayer: DataLayerEvent
menuItemClicked: MenuItemClickedEvent
} }
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> { export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T; type: T;

View file

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

View file

@ -10,4 +10,4 @@ export const isLoadPageEvent =
/** /**
* 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.
*/ */
export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>; export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>;

View file

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

View file

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

View file

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

View file

@ -1,26 +1,42 @@
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 { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; IframeEvent,
import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; IframeEventMap,
import { isLoadPageEvent } from './Events/LoadPageEvent'; IframeResponseEvent,
IframeResponseEventMap,
isIframeEventWrapper,
TypedMessageEvent
} from "./Events/IframeEvent";
import type {UserInputChatEvent} from "./Events/UserInputChatEvent";
//import { isLoadPageEvent } from './Events/LoadPageEvent';
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; import {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 {isLayerEvent, LayerEvent} from "./Events/LayerEvent";
import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent";
import type {DataLayerEvent} from "./Events/DataLayerEvent";
import type {GameStateEvent} from "./Events/GameStateEvent";
import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent";
import {isLoadPageEvent} from "./Events/LoadPageEvent";
import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent";
/** /**
* 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();
@ -33,7 +49,6 @@ class IframeListener {
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject(); private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
public readonly goToPageStream = this._goToPageStream.asObservable(); public readonly goToPageStream = this._goToPageStream.asObservable();
private readonly _loadPageStream: Subject<string> = new Subject(); private readonly _loadPageStream: Subject<string> = new Subject();
public readonly loadPageStream = this._loadPageStream.asObservable(); public readonly loadPageStream = this._loadPageStream.asObservable();
@ -58,6 +73,27 @@ class IframeListener {
private readonly _removeBubbleStream: Subject<void> = new Subject(); private readonly _removeBubbleStream: Subject<void> = new Subject();
public readonly removeBubbleStream = this._removeBubbleStream.asObservable(); public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
private readonly _showLayerStream: Subject<LayerEvent> = new Subject();
public readonly showLayerStream = this._showLayerStream.asObservable();
private readonly _hideLayerStream: Subject<LayerEvent> = new Subject();
public readonly hideLayerStream = this._hideLayerStream.asObservable();
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
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();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
private readonly _unregisterMenuCommandStream: Subject<string> = new Subject();
public readonly unregisterMenuCommandStream = this._unregisterMenuCommandStream.asObservable();
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject(); private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
public readonly playSoundStream = this._playSoundStream.asObservable(); public readonly playSoundStream = this._playSoundStream.asObservable();
@ -68,7 +104,9 @@ class IframeListener {
public readonly loadSoundStream = this._loadSoundStream.asObservable(); public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>(); private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false;
init() { init() {
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => { window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
@ -77,26 +115,27 @@ class IframeListener {
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain). // 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 foundSrc: string | undefined;
foundSrc = [...this.scripts.keys()].find(key => { let iframe: HTMLIFrameElement;
return this.scripts.get(key)?.contentWindow == message.source for (iframe of this.iframes) {
}); if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
}
}
if (foundSrc === undefined) { if (foundSrc === undefined) {
for (const iframe of this.iframes) { return;
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
}
}
if (foundSrc === undefined) {
return;
}
} }
const payload = message.data; const payload = message.data;
if (isIframeEventWrapper(payload)) { if (isIframeEventWrapper(payload)) {
if (payload.type === 'chat' && isChatEvent(payload.data)) { 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); this._chatStream.next(payload.data);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) { } else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data); this._openPopupStream.next(payload.data);
@ -109,6 +148,9 @@ class IframeListener {
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) { else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url); 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)) { else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data); this._playSoundStream.next(payload.data);
} }
@ -131,30 +173,56 @@ class IframeListener {
} }
else if (payload.type === 'restorePlayerControls') { else if (payload.type === 'restorePlayerControls') {
this._enablePlayerControlStream.next(); this._enablePlayerControlStream.next();
} } else if (payload.type === 'displayBubble') {
else if (payload.type === 'displayBubble') {
this._displayBubbleStream.next(); this._displayBubbleStream.next();
} } else if (payload.type === 'removeBubble') {
else if (payload.type === 'removeBubble') {
this._removeBubbleStream.next(); this._removeBubbleStream.next();
}else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){ } else if (payload.type == "getState") {
this._loadPageStream.next(payload.data.url); 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)
} }
} }
}, false); }, false);
} }
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({
'type' : 'dataLayer',
'data' : dataLayerEvent
})
}
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
'type': 'gameState',
'data': gameStateEvent
});
}
/** /**
* Allows the passed iFrame to send/receive messages via the API. * Allows the passed iFrame to send/receive messages via the API.
*/ */
registerIframe(iframe: HTMLIFrameElement): void { registerIframe(iframe: HTMLIFrameElement): void {
this.iframes.add(iframe); this.iframes.add(iframe);
this.iframeCloseCallbacks.set(iframe, []);
} }
unregisterIframe(iframe: HTMLIFrameElement): void { unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframeCloseCallbacks.get(iframe)?.forEach(callback => {
callback();
});
this.iframes.delete(iframe); this.iframes.delete(iframe);
} }
@ -164,7 +232,7 @@ class IframeListener {
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 = this.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);
@ -179,25 +247,24 @@ class IframeListener {
} else { } else {
// production code // production code
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.id = this.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');
const html = '<!doctype html>\n' + //iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = '<!doctype html>\n' +
'\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="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
'<script src="' + scriptUrl + '" ></script>\n' + '<script src="' + scriptUrl + '" ></script>\n' +
'<title></title>\n' +
'</head>\n' + '</head>\n' +
'</html>\n'; '</html>\n';
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = html;
document.body.prepend(iframe); document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe); this.scripts.set(scriptUrl, iframe);
@ -207,12 +274,12 @@ class IframeListener {
} }
private 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 {
const iFrameId = this.getIFrameId(scriptUrl); const iFrameId = IframeListener.getIFrameId(scriptUrl);
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId); const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
if (!iframe) { if (!iframe) {
throw new Error('Unknown iframe for script "' + scriptUrl + '"'); throw new Error('Unknown iframe for script "' + scriptUrl + '"');
@ -250,6 +317,15 @@ class IframeListener {
}); });
} }
hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) {
this.postMessage({
'type': 'hasPlayerMoved',
'data': event
});
}
}
sendButtonClickedEvent(popupId: number, buttonId: number): void { sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({ this.postMessage({
'type': 'buttonClickedEvent', 'type': 'buttonClickedEvent',
@ -263,7 +339,7 @@ class IframeListener {
/** /**
* Sends the message... to all allowed iframes. * Sends the message... to all allowed iframes.
*/ */
private 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, '*');
} }

View file

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

View file

@ -23,7 +23,7 @@ class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatC
data: { data: {
'message': message, 'message': message,
'author': author 'author': author
} as ChatEvent }
}) })
} }

View file

@ -2,6 +2,7 @@ import type { GoToPageEvent } from '../Events/GoToPageEvent';
import type { OpenTabEvent } from '../Events/OpenTabEvent'; import type { OpenTabEvent } from '../Events/OpenTabEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent"; import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent";
import type {LoadPageEvent} from "../Events/LoadPageEvent";
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> { class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
@ -13,7 +14,7 @@ class WorkadventureNavigationCommands extends IframeApiContribution<Workadventur
"type": 'openTab', "type": 'openTab',
"data": { "data": {
url url
} as OpenTabEvent }
}); });
} }
@ -22,7 +23,7 @@ class WorkadventureNavigationCommands extends IframeApiContribution<Workadventur
"type": 'goToPage', "type": 'goToPage',
"data": { "data": {
url url
} as GoToPageEvent }
}); });
} }
@ -40,7 +41,7 @@ class WorkadventureNavigationCommands extends IframeApiContribution<Workadventur
"type": 'openCoWebSite', "type": 'openCoWebSite',
"data": { "data": {
url url
} as OpenCoWebSiteEvent }
}); });
} }

View file

@ -0,0 +1,29 @@
import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContribution";
import type {HasPlayerMovedEvent, HasPlayerMovedEventCallback} from "../Events/HasPlayerMovedEvent";
import {Subject} from "rxjs";
import {apiCallback} from "./registeredCallbacks";
import {isHasPlayerMovedEvent} from "../Events/HasPlayerMovedEvent";
const moveStream = new Subject<HasPlayerMovedEvent>();
class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
callbacks = [
apiCallback({
type: 'hasPlayerMoved',
typeChecker: isHasPlayerMovedEvent,
callback: (payloadData) => {
moveStream.next(payloadData);
}
}),
]
onPlayerMove(callback: HasPlayerMovedEventCallback): void {
moveStream.subscribe(callback);
sendToWorkadventure({
type: 'onPlayerMove',
data: null
})
}
}
export default new WorkadventurePlayerCommands();

View file

@ -1,10 +1,54 @@
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent'; import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent';
import { IframeApiContribution } from './IframeApiContribution'; import {IframeApiContribution, sendToWorkadventure} from './IframeApiContribution';
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import type {LayerEvent} from "../Events/LayerEvent";
import type {SetPropertyEvent} from "../Events/setPropertyEvent";
import type {GameStateEvent} from "../Events/GameStateEvent";
import type {ITiledMap} from "../../Phaser/Map/ITiledMap";
import type {DataLayerEvent} from "../Events/DataLayerEvent";
import {isGameStateEvent} from "../Events/GameStateEvent";
import {isDataLayerEvent} from "../Events/DataLayerEvent";
const enterStreams: Map<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 stateResolvers = new Subject<GameStateEvent>();
let immutableData: GameStateEvent;
interface Room {
id: string,
mapUrl: string,
map: ITiledMap,
startLayer: string | null
}
interface User {
id: string | undefined,
nickName: string | null,
tags: string[]
}
function getGameState(): Promise<GameStateEvent> {
if (immutableData) {
return Promise.resolve(immutableData);
}
else {
return new Promise<GameStateEvent>((resolver, thrower) => {
stateResolvers.subscribe(resolver);
sendToWorkadventure({type: "getState", data: null});
})
}
}
function getDataLayer(): Promise<DataLayerEvent> {
return new Promise<DataLayerEvent>((resolver, thrower) => {
dataLayerResolver.subscribe(resolver);
sendToWorkadventure({type: "getDataLayer", data: null})
})
}
class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> { class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
callbacks = [ callbacks = [
@ -21,8 +65,21 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
callback: (payloadData) => { callback: (payloadData) => {
leaveStreams.get(payloadData.name)?.next(); leaveStreams.get(payloadData.name)?.next();
} }
}) }),
apiCallback({
type: "gameState",
typeChecker: isGameStateEvent,
callback: (payloadData) => {
stateResolvers.next(payloadData);
}
}),
apiCallback({
type: "dataLayer",
typeChecker: isDataLayerEvent,
callback: (payloadData) => {
dataLayerResolver.next(payloadData);
}
}),
] ]
@ -43,6 +100,34 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
} }
subject.subscribe(callback); subject.subscribe(callback);
} }
showLayer(layerName: string): void {
sendToWorkadventure({type: 'showLayer', data: {'name': layerName}});
}
hideLayer(layerName: string): void {
sendToWorkadventure({type: 'hideLayer', data: {'name': layerName}});
}
setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void {
sendToWorkadventure({
type: 'setProperty',
data: {
'layerName': layerName,
'propertyName': propertyName,
'propertyValue': propertyValue,
}
})
}
getCurrentRoom(): Promise<Room> {
return getGameState().then((gameState) => {
return getDataLayer().then((mapJson) => {
return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName};
})
})
}
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags};
})
}
} }

View file

@ -1,14 +1,17 @@
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
import type { ClosePopupEvent } from '../Events/ClosePopupEvent'; import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent';
import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import {Popup} from "./Ui/Popup"; import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import type {ButtonClickedCallback, ButtonDescriptor} from "./Ui/ButtonDescriptor"; import { Popup } from "./Ui/Popup";
let popupId = 0; let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>(); const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>(); const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
const menuCallbacks: Map<string, (command: string) => void> = new Map()
interface ZonedPopupOptions { interface ZonedPopupOptions {
zone: string zone: string
objectLayerName?: string, objectLayerName?: string,
@ -33,6 +36,16 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
callback(popup); callback(popup);
} }
} }
}),
apiCallback({
type: "menuItemClicked",
typeChecker: isMenuItemClickedEvent,
callback: event => {
const callback = menuCallbacks.get(event.menuItem);
if (callback) {
callback(event.menuItem)
}
}
})]; })];
@ -71,6 +84,16 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
return popup; return popup;
} }
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({
'type': 'registerMenuCommand',
'data': {
menutItem: commandDescriptor
}
});
}
displayBubble(): void { displayBubble(): void {
sendToWorkadventure({ 'type': 'displayBubble', data: null }); sendToWorkadventure({ 'type': 'displayBubble', data: null });
} }

View file

@ -23,6 +23,8 @@
import ErrorDialog from "./UI/ErrorDialog.svelte"; import ErrorDialog from "./UI/ErrorDialog.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte"; import VideoOverlay from "./Video/VideoOverlay.svelte";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
export let game: Game; export let game: Game;
@ -74,6 +76,11 @@
<CameraControls></CameraControls> <CameraControls></CameraControls>
</div> </div>
{/if} {/if}
{#if $consoleGlobalMessageManagerVisibleStore}
<div>
<ConsoleGlobalMessageManager game={game}></ConsoleGlobalMessageManager>
</div>
{/if}
{#if $helpCameraSettingsVisibleStore} {#if $helpCameraSettingsVisibleStore}
<div> <div>
<HelpCameraSettingsPopup></HelpCameraSettingsPopup> <HelpCameraSettingsPopup></HelpCameraSettingsPopup>

View file

@ -0,0 +1,44 @@
<script lang="typescript">
import InputTextGlobalMessage from "./InputTextGlobalMessage.svelte";
import UploadAudioGlobalMessage from "./UploadAudioGlobalMessage.svelte";
import {gameManager} from "../../Phaser/Game/GameManager";
import type {Game} from "../../Phaser/Game/Game";
export let game: Game;
let inputSendTextActive = true;
let uploadMusicActive = false;
function inputSendTextActivate() {
inputSendTextActive = true;
uploadMusicActive = false;
}
function inputUploadMusicActivate() {
uploadMusicActive = true;
inputSendTextActive = false;
}
</script>
<div class="main-console nes-container is-rounded">
<!-- <div class="console nes-container is-rounded">
<img class="btn-close" src="resources/logos/send-yellow.svg" alt="Close">
</div>-->
<div class="main-global-message">
<h2> Global Message </h2>
<div class="global-message">
<div class="menu">
<button class="nes-btn {inputSendTextActive ? 'is-disabled' : ''}" on:click|preventDefault={inputSendTextActivate}>Message</button>
<button class="nes-btn {uploadMusicActive ? 'is-disabled' : ''}" on:click|preventDefault={inputUploadMusicActivate}>Audio</button>
</div>
<div class="main-input">
{#if inputSendTextActive}
<InputTextGlobalMessage game={game} gameManager={gameManager}></InputTextGlobalMessage>
{/if}
{#if uploadMusicActive}
<UploadAudioGlobalMessage game={game} gameManager={gameManager}></UploadAudioGlobalMessage>
{/if}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,99 @@
<script lang="ts">
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import {onMount} from "svelte";
import type {Game} from "../../Phaser/Game/Game";
import type {GameManager} from "../../Phaser/Game/GameManager";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService";
import type {Quill} from "quill";
import {LoginSceneName} from "../../Phaser/Login/LoginScene";
//toolbar
export const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
[{'header': 1}, {'header': 2}], // custom button values
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'script': 'sub'}, {'script': 'super'}], // superscript/subscript
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
[{'direction': 'rtl'}], // text direction
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}], // dropdown with defaults from theme
[{'font': []}],
[{'align': []}],
['clean'],
['link', 'image', 'video']
// remove formatting button
];
export let game: Game;
export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.scene.getScene(LoginSceneName));
let quill: Quill;
let INPUT_CONSOLE_MESSAGE: HTMLDivElement;
const MESSAGE_TYPE = AdminMessageEventTypes.admin;
//Quill
onMount(async () => {
// Import quill
const {default: Quill} = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
quill = new Quill(INPUT_CONSOLE_MESSAGE, {
theme: 'snow',
modules: {
toolbar: toolbarOptions
},
});
quill.on('selection-change', function (range, oldRange) {
if (range === null && oldRange !== null) {
consoleGlobalMessageManagerFocusStore.set(false);
} else if (range !== null && oldRange === null)
consoleGlobalMessageManagerFocusStore.set(true);
});
});
function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false);
}
function SendTextMessage() {
if (gameScene == undefined) {
return;
}
const text = quill.getText(0, quill.getLength());
const GlobalMessage: PlayGlobalMessageInterface = {
id: "1", // FIXME: use another ID?
message: text,
type: MESSAGE_TYPE
};
quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(GlobalMessage);
disableConsole();
}
</script>
<section class="section-input-send-text">
<div class="input-send-text" bind:this={INPUT_CONSOLE_MESSAGE}></div>
<div class="btn-action">
<button class="nes-btn is-primary" on:click|preventDefault={SendTextMessage}>Send</button>
</div>
</section>
<style lang="scss">
@import 'https://cdn.quilljs.com/1.3.7/quill.snow.css';
</style>

View file

@ -0,0 +1,130 @@
<script lang="ts">
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import type {Game} from "../../Phaser/Game/Game";
import type {GameManager} from "../../Phaser/Game/GameManager";
import {consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
import {AdminMessageEventTypes} from "../../Connexion/AdminMessagesService";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
import uploadFile from "../images/music-file.svg";
import {LoginSceneName} from "../../Phaser/Login/LoginScene";
interface EventTargetFiles extends EventTarget {
files: Array<File>;
}
export let game: Game;
export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.scene.getScene(LoginSceneName));
let fileinput: HTMLInputElement;
let filename: string;
let filesize: string;
let errorfile: boolean;
const AUDIO_TYPE = AdminMessageEventTypes.audio;
async function SendAudioMessage() {
if (gameScene == undefined) {
return;
}
const inputAudio = HtmlUtils.getElementByIdOrFail<HTMLInputElement>("input-send-audio");
const selectedFile = inputAudio.files ? inputAudio.files[0] : null;
if (!selectedFile) {
errorfile = true;
throw 'no file selected';
}
const fd = new FormData();
fd.append('file', selectedFile);
const res = await gameScene.connection?.uploadAudio(fd);
const GlobalMessage: PlayGlobalMessageInterface = {
id: (res as { id: string }).id,
message: (res as { path: string }).path,
type: AUDIO_TYPE
}
inputAudio.value = '';
gameScene.connection?.emitGlobalMessage(GlobalMessage);
disableConsole();
}
function inputAudioFile(event: Event) {
const eventTarget : EventTargetFiles = (event.target as EventTargetFiles);
if(!eventTarget || !eventTarget.files || eventTarget.files.length === 0){
return;
}
const file = eventTarget.files[0];
if(!file) {
return;
}
filename = file.name;
filesize = getFileSize(file.size);
errorfile = false;
}
function getFileSize(number: number) {
if (number < 1024) {
return number + 'bytes';
} else if (number >= 1024 && number < 1048576) {
return (number / 1024).toFixed(1) + 'KB';
} else if (number >= 1048576) {
return (number / 1048576).toFixed(1) + 'MB';
} else {
return '';
}
}
function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false);
}
</script>
<section class="section-input-send-audio">
<div class="input-send-audio">
<img src="{uploadFile}" alt="Upload a file" on:click|preventDefault={ () => {fileinput.click();}}>
{#if filename != undefined}
<label for="input-send-audio">{filename} : {filesize}</label>
{/if}
{#if errorfile}
<p class="err">No file selected. You need to upload a file before sending it.</p>
{/if}
<input type="file" id="input-send-audio" bind:this={fileinput} on:change={(e) => {inputAudioFile(e)}}>
</div>
<div class="btn-action">
<button class="nes-btn is-primary" on:click|preventDefault={SendAudioMessage}>Send</button>
</div>
</section>
<style lang="scss">
//UploadAudioGlobalMessage
.section-input-send-audio {
margin: 10px;
}
.section-input-send-audio .input-send-audio {
text-align: center;
}
.section-input-send-audio #input-send-audio{
display: none;
}
.section-input-send-audio div.input-send-audio label{
color: white;
}
.section-input-send-audio div.input-send-audio p.err {
color: #ce372b;
text-align: center;
}
.section-input-send-audio div.input-send-audio img{
height: 150px;
cursor: url('../../../style/images/cursor_pointer.png'), pointer;
}
</style>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 448 448" style="enable-background:new 0 0 448 448;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFDA01;}
</style>
<path class="st0" d="M348,288c-44.2,0-80,35.8-80,80s35.8,80,80,80s80-35.8,80-80C428,323.8,392.2,288,348,288z M387.6,359.6
c-3.1,3.1-8.2,3.1-11.3,0L356,339.3V416c0,4.4-3.6,8-8,8s-8-3.6-8-8v-76.7l-20.3,20.3c-3.1,3-8.1,3-11.2-0.1s-3.1-8.1-0.1-11.2
l33.9-33.9c0.7-0.7,1.6-1.3,2.6-1.7c2-0.8,4.2-0.8,6.1,0c1,0.4,1.9,1,2.6,1.7l33.9,33.9C390.7,351.4,390.7,356.5,387.6,359.6z"/>
<path class="st0" d="M244,154.6L148,182v15.4l96-27.4V154.6z"/>
<path class="st0" d="M244,280c0,8.8-7.2,16-16,16s-16-7.2-16-16s7.2-16,16-16S244,271.2,244,280z"/>
<path class="st0" d="M132,312c0,8.8-7.2,16-16,16s-16-7.2-16-16s7.2-16,16-16S132,303.2,132,312z"/>
<path class="st0" d="M31.3,80H100V11.3L31.3,80z"/>
<path class="st0" d="M20,448h275c-0.1-0.1-0.2-0.1-0.3-0.2c-2.9-2-5.8-4.1-8.5-6.4c-0.7-0.6-1.4-1.3-2.1-1.9
c-1.9-1.7-3.8-3.5-5.6-5.4c-0.8-0.9-1.6-1.8-2.4-2.7c-1.6-1.8-3.2-3.8-4.7-5.7c-0.7-0.9-1.4-1.8-2-2.7c-1.8-2.6-3.5-5.2-5-8
c-0.2-0.4-0.4-0.7-0.6-1c-1.7-3.1-3.2-6.4-4.6-9.7c-0.4-1-0.7-2-1.1-3c-0.9-2.4-1.7-4.9-2.4-7.4c-0.3-1.2-0.6-2.4-0.9-3.6
c-0.6-2.5-1.1-5-1.5-7.6c-0.2-1.1-0.4-2.3-0.5-3.4c-0.9-6.9-1-13.9-0.2-20.8c0.1-1.1,0.3-2.1,0.5-3.1c0.3-2.1,0.5-4.1,0.9-6.2
c0.2-1.2,0.6-2.4,0.9-3.7c0.4-1.8,0.8-3.6,1.4-5.3c0.4-1.3,0.9-2.5,1.3-3.8c0.6-1.6,1.1-3.3,1.8-4.9c0.5-1.3,1.1-2.5,1.7-3.7
c0.7-1.5,1.4-3,2.2-4.5c0.6-1.2,1.4-2.4,2-3.6c0.8-1.4,1.7-2.8,2.6-4.2c0.8-1.2,1.6-2.3,2.4-3.4c0.9-1.3,1.9-2.6,2.9-3.9
c0.9-1.1,1.8-2.1,2.7-3.2c1.1-1.2,2.1-2.4,3.2-3.6c1-1,2-2,3-2.9c1.2-1.1,2.3-2.2,3.6-3.2c1.1-0.9,2.1-1.8,3.2-2.7
c1.3-1,2.6-2,3.9-2.9c1.1-0.8,2.3-1.6,3.5-2.4c1.4-0.9,2.8-1.7,4.2-2.5c1.2-0.7,2.4-1.4,3.6-2c1.5-0.8,2.9-1.5,4.4-2.1
c1.3-0.6,2.5-1.2,3.8-1.7c1.6-0.6,3.1-1.2,4.7-1.7c1.3-0.4,2.6-0.9,3.9-1.3c1.6-0.5,3.3-0.9,5-1.3c1.3-0.3,2.6-0.7,4-0.9
c1.8-0.3,3.5-0.6,5.3-0.8c1.3-0.2,2.6-0.4,4-0.5c0.3,0,0.6-0.1,1-0.1V0H116v88c0,4.4-3.6,8-8,8H20V448z M116,280
c5.6,0,11.2,1.6,16,4.4V176c0-3.6,2.4-6.7,5.8-7.7l112-32c2.4-0.7,5-0.2,7,1.3s3.2,3.9,3.2,6.4v136c0,17.7-14.3,32-32,32
s-32-14.3-32-32s14.3-32,32-32c5.6,0,11.2,1.6,16,4.4v-65.8L148,214v98c0,17.7-14.3,32-32,32s-32-14.3-32-32S98.3,280,116,280z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -10,7 +10,7 @@ export interface CharacterTexture {
export const maxUserNameLength: number = MAX_USERNAME_LENGTH; export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
export function isUserNameValid(value: unknown): boolean { export function isUserNameValid(value: unknown): boolean {
return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && value.indexOf(' ') === -1; return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && /\S/.test(value);
} }
export function areCharacterLayersValid(value: string[] | null): boolean { export function areCharacterLayersValid(value: string[] | null): boolean {

View file

@ -1,4 +1,4 @@
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; import { PUSHER_URL, UPLOADER_URL } from "../Enum/EnvironmentVariable";
import Axios from "axios"; import Axios from "axios";
import { import {
BatchMessage, BatchMessage,
@ -33,9 +33,9 @@ import {
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, ItemEventMessageInterface,
@ -44,24 +44,24 @@ import {
ViewportInterface, WebRtcDisconnectMessageInterface, ViewportInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels"; } from "./ConnexionModels";
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import {adminMessagesService} from "./AdminMessagesService"; import { adminMessagesService } from "./AdminMessagesService";
import {worldFullMessageStream} from "./WorldFullMessageStream"; import { worldFullMessageStream } from "./WorldFullMessageStream";
import {worldFullWarningStream} from "./WorldFullWarningStream"; import { worldFullWarningStream } from "./WorldFullWarningStream";
import {connectionManager} from "./ConnectionManager"; import { connectionManager } from "./ConnectionManager";
import {emoteEventStream} from "./EmoteEventStream"; import { emoteEventStream } from "./EmoteEventStream";
const manualPingDelay = 20000; const manualPingDelay = 20000;
export class RoomConnection implements RoomConnection { export class RoomConnection implements RoomConnection {
private readonly socket: WebSocket; private readonly socket: WebSocket;
private userId: number|null = null; private userId: number | null = null;
private listeners: Map<string, Function[]> = new Map<string, Function[]>(); private listeners: Map<string, Function[]> = new Map<string, Function[]>();
private static websocketFactory: null|((url: string)=>any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any private static websocketFactory: null | ((url: string) => any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any
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 public static setWebsocketFactory(websocketFactory: (url: string) => any): void { // eslint-disable-line @typescript-eslint/no-explicit-any
RoomConnection.websocketFactory = websocketFactory; RoomConnection.websocketFactory = websocketFactory;
} }
@ -70,28 +70,27 @@ export class RoomConnection implements RoomConnection {
* @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 roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[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, roomId: 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=' + (roomId ? encodeURIComponent(roomId) : '');
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) {
@ -102,7 +101,7 @@ export class RoomConnection implements RoomConnection {
this.socket.binaryType = 'arraybuffer'; this.socket.binaryType = 'arraybuffer';
let interval: ReturnType<typeof setInterval>|undefined = undefined; let interval: ReturnType<typeof setInterval> | undefined = undefined;
this.socket.onopen = (ev) => { this.socket.onopen = (ev) => {
//we manually ping every 20s to not be logged out by the server, even when the game is in background. //we manually ping every 20s to not be logged out by the server, even when the game is in background.
@ -161,7 +160,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasRoomjoinedmessage()) { } else if (message.hasRoomjoinedmessage()) {
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage; const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
const items: { [itemId: number] : unknown } = {}; const items: { [itemId: number]: unknown } = {};
for (const item of roomJoinedMessage.getItemList()) { for (const item of roomJoinedMessage.getItemList()) {
items[item.getItemid()] = JSON.parse(item.getStatejson()); items[item.getItemid()] = JSON.parse(item.getStatejson());
} }
@ -181,7 +180,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasWorldconnexionmessage()) { } else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
this.closed = true; this.closed = true;
}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());
@ -238,7 +237,7 @@ export class RoomConnection implements RoomConnection {
this.closed = true; this.closed = true;
} }
private toPositionMessage(x : number, y : number, direction : string, moving: boolean): PositionMessage { private toPositionMessage(x: number, y: number, direction: string, moving: boolean): PositionMessage {
const positionMessage = new PositionMessage(); const positionMessage = new PositionMessage();
positionMessage.setX(Math.floor(x)); positionMessage.setX(Math.floor(x));
positionMessage.setY(Math.floor(y)); positionMessage.setY(Math.floor(y));
@ -275,8 +274,8 @@ export class RoomConnection implements RoomConnection {
return viewportMessage; return viewportMessage;
} }
public sharePosition(x : number, y : number, direction : string, moving: boolean, viewport: ViewportInterface) : void{ public sharePosition(x: number, y: number, direction: string, moving: boolean, viewport: ViewportInterface): void {
if(!this.socket){ if (!this.socket) {
return; return;
} }
@ -481,7 +480,7 @@ export class RoomConnection implements RoomConnection {
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;
@ -527,8 +526,8 @@ export class RoomConnection implements RoomConnection {
}); });
} }
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).then((res: { data: {} }) => {
return res.data; return res.data;
}).catch((err) => { }).catch((err) => {
console.error(err); console.error(err);
@ -559,7 +558,7 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public emitGlobalMessage(message: PlayGlobalMessageInterface){ public emitGlobalMessage(message: PlayGlobalMessageInterface) {
const playGlobalMessage = new PlayGlobalMessage(); const playGlobalMessage = new PlayGlobalMessage();
playGlobalMessage.setId(message.id); playGlobalMessage.setId(message.id);
playGlobalMessage.setType(message.type); playGlobalMessage.setType(message.type);
@ -571,7 +570,7 @@ 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(reportedUserId: number, reportComment: string): void {
const reportPlayerMessage = new ReportPlayerMessage(); const reportPlayerMessage = new ReportPlayerMessage();
reportPlayerMessage.setReporteduserid(reportedUserId); reportPlayerMessage.setReporteduserid(reportedUserId);
reportPlayerMessage.setReportcomment(reportComment); reportPlayerMessage.setReportcomment(reportComment);
@ -582,7 +581,7 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string|undefined ): void { public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string | undefined): void {
const queryJitsiJwtMessage = new QueryJitsiJwtMessage(); const queryJitsiJwtMessage = new QueryJitsiJwtMessage();
queryJitsiJwtMessage.setJitsiroom(jitsiRoom); queryJitsiJwtMessage.setJitsiroom(jitsiRoom);
if (tag !== undefined) { if (tag !== undefined) {
@ -618,4 +617,8 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public getAllTags() : string[] {
return this.tags;
}
} }

View file

@ -8,6 +8,7 @@ import {Companion} from "../Companion/Companion";
import type {GameScene} from "../Game/GameScene"; import type {GameScene} from "../Game/GameScene";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager"; import {waScaleManager} from "../Services/WaScaleManager";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
const playerNameY = - 25; const playerNameY = - 25;
@ -32,6 +33,7 @@ export abstract class Character extends Container {
public companion?: Companion; public companion?: Companion;
private emote: Phaser.GameObjects.Sprite | null = null; private emote: Phaser.GameObjects.Sprite | null = null;
private emoteTween: Phaser.Tweens.Tween|null = null; private emoteTween: Phaser.Tweens.Tween|null = null;
scene: GameScene;
constructor(scene: GameScene, constructor(scene: GameScene,
x: number, x: number,
@ -46,6 +48,7 @@ export abstract class Character extends Container {
companionTexturePromise?: Promise<string> companionTexturePromise?: Promise<string>
) { ) {
super(scene, x, y/*, texture, frame*/); super(scene, x, y/*, texture, frame*/);
this.scene = scene;
this.PlayerValue = name; this.PlayerValue = name;
this.invisible = true this.invisible = true
@ -67,6 +70,19 @@ export abstract class Character extends Container {
hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method
useHandCursor: true, useHandCursor: true,
}); });
this.on('pointerover',() => {
this.getOutlinePlugin()?.add(this.playerName, {
thickness: 2,
outlineColor: 0xffff00
});
this.scene.markDirty();
});
this.on('pointerout',() => {
this.getOutlinePlugin()?.remove(this.playerName);
this.scene.markDirty();
})
} }
scene.add.existing(this); scene.add.existing(this);
@ -86,6 +102,10 @@ export abstract class Character extends Container {
} }
} }
private getOutlinePlugin(): OutlinePipelinePlugin|undefined {
return this.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined;
}
public addCompanion(name: string, texturePromise?: Promise<string>): void { public addCompanion(name: string, texturePromise?: Promise<string>): void {
if (typeof texturePromise !== 'undefined') { if (typeof texturePromise !== 'undefined') {
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise); this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);

View file

@ -37,6 +37,7 @@ export abstract class DirtyScene extends ResizableScene {
this.events.on(Events.RENDER, () => { this.events.on(Events.RENDER, () => {
this.objectListChanged = false; this.objectListChanged = false;
this.dirty = false;
}); });
this.physics.disableUpdate(); this.physics.disableUpdate();
@ -69,6 +70,10 @@ export abstract class DirtyScene extends ResizableScene {
return this.dirty || this.objectListChanged; return this.dirty || this.objectListChanged;
} }
public markDirty(): void {
this.events.once(Phaser.Scenes.Events.POST_UPDATE, () => this.dirty = true);
}
public onResize(): void { public onResize(): void {
this.objectListChanged = true; this.objectListChanged = true;
} }

View file

@ -10,12 +10,7 @@ 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";
export interface HasMovedEvent {
direction: string;
moving: boolean;
x: number;
y: number;
}
/** /**
* This class should be responsible for any scene starting/stopping * This class should be responsible for any scene starting/stopping

View file

@ -1,5 +1,7 @@
import type {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap"; import type {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty} from "../Map/ITiledMap";
import {LayersIterator} from "../Map/LayersIterator"; import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
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;
@ -8,15 +10,43 @@ export type PropertyChangeCallback = (newValue: string | number | boolean | unde
* It is used to handle layer properties. * It is used to handle layer properties.
*/ */
export class GameMap { export class GameMap {
private key: number|undefined; private key: number | undefined;
private lastProperties = new Map<string, string|boolean|number>(); private lastProperties = new Map<string, string | boolean | number>();
private callbacks = new Map<string, Array<PropertyChangeCallback>>(); private callbacks = new Map<string, Array<PropertyChangeCallback>>();
public readonly layersIterator: LayersIterator;
public constructor(private map: ITiledMap) { private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {}
this.layersIterator = new LayersIterator(map); public readonly flatLayers: ITiledMapLayer[];
public readonly phaserLayers: TilemapLayer[] = [];
public exitUrls: Array<string> = []
public constructor(private map: ITiledMap, phaserMap: Phaser.Tilemaps.Tilemap, terrains: Array<Phaser.Tilemaps.Tileset>) {
this.flatLayers = flattenGroupLayersMap(map);
let depth = -2;
for (const layer of this.flatLayers) {
if(layer.type === 'tilelayer'){
this.phaserLayers.push(phaserMap.createLayer(layer.name, terrains, 0, 0).setDepth(depth));
}
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = DEPTH_OVERLAY_INDEX;
}
}
for (const tileset of map.tilesets) {
tileset?.tiles?.forEach(tile => {
if (tile.properties) {
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties
tile.properties.forEach(prop => {
if (prop.name == "exitUrl" && typeof prop.value == "string") {
this.exitUrls.push(prop.value);
}
})
}
})
}
} }
/** /**
* Sets the position of the current player (in pixels) * Sets the position of the current player (in pixels)
* This will trigger events if properties are changing. * This will trigger events if properties are changing.
@ -51,21 +81,27 @@ export class GameMap {
} }
} }
public getCurrentProperties(): Map<string, string|boolean|number> { public getCurrentProperties(): Map<string, string | boolean | number> {
return this.lastProperties; return this.lastProperties;
} }
private getProperties(key: number): Map<string, string|boolean|number> { private getProperties(key: number): Map<string, string | boolean | number> {
const properties = new Map<string, string|boolean|number>(); const properties = new Map<string, string | boolean | number>();
for (const layer of this.layersIterator) { for (const layer of this.flatLayers) {
if (layer.type !== 'tilelayer') { if (layer.type !== 'tilelayer') {
continue; continue;
} }
const tiles = layer.data as number[];
if (tiles[key] == 0) { let tileIndex: number | undefined = undefined;
continue; if (layer.data) {
const tiles = layer.data as number[];
if (tiles[key] == 0) {
continue;
}
tileIndex = tiles[key]
} }
// There is a tile in this layer, let's embed the properties // There is a tile in this layer, let's embed the properties
if (layer.properties !== undefined) { if (layer.properties !== undefined) {
for (const layerProperty of layer.properties) { for (const layerProperty of layer.properties) {
@ -75,11 +111,25 @@ export class GameMap {
properties.set(layerProperty.name, layerProperty.value); properties.set(layerProperty.name, layerProperty.value);
} }
} }
if (tileIndex) {
this.tileSetPropertyMap[tileIndex]?.forEach(property => {
if (property.value) {
properties.set(property.name, property.value)
} else if (properties.has(property.name)) {
properties.delete(property.name)
}
})
}
} }
return properties; return properties;
} }
public getMap(): ITiledMap{
return this.map;
}
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) {
@ -100,4 +150,19 @@ export class GameMap {
} }
callbacksArray.push(callback); callbacksArray.push(callback);
} }
public findLayer(layerName: string): ITiledMapLayer | undefined {
return this.flatLayers.find((layer) => layer.name === layerName);
}
public findPhaserLayer(layerName: string): TilemapLayer | undefined {
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
}
public addTerrain(terrain : Phaser.Tilemaps.Tileset): void {
for (const phaserLayer of this.phaserLayers) {
phaserLayer.tileset.push(terrain);
}
}
} }

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
import type {HasMovedEvent} from "./GameManager"; 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';
export class PlayerMovement { export class PlayerMovement {
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) { public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasPlayerMovedEvent, private endTick: number) {
} }
public isOutdated(tick: number): boolean { public isOutdated(tick: number): boolean {
@ -17,7 +17,7 @@ export class PlayerMovement {
return tick > this.endTick + MAX_EXTRAPOLATION_TIME; return tick > this.endTick + MAX_EXTRAPOLATION_TIME;
} }
public getPosition(tick: number): HasMovedEvent { public getPosition(tick: number): HasPlayerMovedEvent {
// Special case: end position reached and end position is not moving // Special case: end position reached and end position is not moving
if (tick >= this.endTick && this.endPosition.moving === false) { if (tick >= this.endTick && this.endPosition.moving === false) {
//console.log('Movement finished ', this.endPosition) //console.log('Movement finished ', this.endPosition)

View file

@ -2,13 +2,13 @@
* 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 {PlayerMovement} from "./PlayerMovement"; import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
import type {HasMovedEvent} from "./GameManager"; import type { PlayerMovement } from "./PlayerMovement";
export class PlayersPositionInterpolator { export class PlayersPositionInterpolator {
playerMovements: Map<number, PlayerMovement> = new Map<number, PlayerMovement>(); playerMovements: Map<number, PlayerMovement> = new Map<number, PlayerMovement>();
updatePlayerPosition(userId: number, playerMovement: PlayerMovement) : void { updatePlayerPosition(userId: number, playerMovement: PlayerMovement): void {
this.playerMovements.set(userId, playerMovement); this.playerMovements.set(userId, playerMovement);
} }
@ -16,8 +16,8 @@ export class PlayersPositionInterpolator {
this.playerMovements.delete(userId); this.playerMovements.delete(userId);
} }
getUpdatedPositions(tick: number) : Map<number, HasMovedEvent> { getUpdatedPositions(tick: number): Map<number, HasPlayerMovedEvent> {
const positions = new Map<number, HasMovedEvent>(); const positions = new Map<number, HasPlayerMovedEvent>();
this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => { this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => {
if (playerMovement.isOutdated(tick)) { if (playerMovement.isOutdated(tick)) {
//console.log("outdated") //console.log("outdated")

View file

@ -3,8 +3,8 @@
* It has coordinates and an "activation radius" * It has coordinates and an "activation radius"
*/ */
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import {OutlinePipeline} from "../Shaders/OutlinePipeline";
import type {GameScene} from "../Game/GameScene"; import type {GameScene} from "../Game/GameScene";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
type EventCallback = (state: unknown, parameters: unknown) => void; type EventCallback = (state: unknown, parameters: unknown) => void;
@ -42,11 +42,11 @@ export class ActionableItem {
return; return;
} }
this.isSelectable = true; this.isSelectable = true;
if (this.sprite.pipeline) {
// Commented out to try to fix MacOS issue this.getOutlinePlugin()?.add(this.sprite, {
/*this.sprite.setPipeline(OutlinePipeline.KEY); thickness: 2,
this.sprite.pipeline.set2f('uTextureSize', this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height);*/ outlineColor: 0xffff00
} });
} }
/** /**
@ -57,8 +57,11 @@ export class ActionableItem {
return; return;
} }
this.isSelectable = false; this.isSelectable = false;
// Commented out to try to fix MacOS issue this.getOutlinePlugin()?.remove(this.sprite);
//this.sprite.resetPipeline(); }
private getOutlinePlugin(): OutlinePipelinePlugin|undefined {
return this.sprite.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined;
} }
/** /**

View file

@ -4,6 +4,8 @@
* Represents the interface for the Tiled exported data structure (JSON). Used * Represents the interface for the Tiled exported data structure (JSON). Used
* when loading resources via Resource loader. * when loading resources via Resource loader.
*/ */
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
export interface ITiledMap { export interface ITiledMap {
width: number; width: number;
height: number; height: number;
@ -81,6 +83,7 @@ export interface ITiledMapTileLayer {
* Draw order (topdown (default), index) * Draw order (topdown (default), index)
*/ */
draworder?: string; draworder?: string;
phaserLayer?: TilemapLayer;
} }
export interface ITiledMapObjectLayer { export interface ITiledMapObjectLayer {
@ -167,7 +170,7 @@ export interface ITiledTileSet {
tilewidth: number; tilewidth: number;
transparentcolor: string; transparentcolor: string;
terrains: ITiledMapTerrain[]; terrains: ITiledMapTerrain[];
tiles: {[key: string]: { terrain: number[] }}; tiles?: Array<ITile>;
/** /**
* Refers to external tileset file (should be JSON) * Refers to external tileset file (should be JSON)
@ -175,6 +178,13 @@ export interface ITiledTileSet {
source: string; source: string;
} }
export interface ITile {
id: number,
type?: string
properties?: Array<ITiledMapLayerProperty>
}
export interface ITiledMapTerrain { export interface ITiledMapTerrain {
name: string; name: string;
tile: number; tile: number;

View file

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

View file

@ -1,44 +0,0 @@
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap";
/**
* Iterates over the layers of a map, flattening the grouped layers
*/
export class LayersIterator implements IterableIterator<ITiledMapLayer> {
private layers: ITiledMapLayer[] = [];
private pointer: number = 0;
constructor(private map: ITiledMap) {
this.initLayersList(map.layers, '');
}
private initLayersList(layers : ITiledMapLayer[], prefix : string) {
for (const layer of layers) {
if (layer.type === 'group') {
this.initLayersList(layer.layers, prefix + layer.name + '/');
} else {
const layerWithNewName = { ...layer };
layerWithNewName.name = prefix+layerWithNewName.name;
this.layers.push(layerWithNewName);
}
}
}
public next(): IteratorResult<ITiledMapLayer> {
if (this.pointer < this.layers.length) {
return {
done: false,
value: this.layers[this.pointer++]
}
} else {
return {
done: true,
value: null
}
}
}
[Symbol.iterator](): IterableIterator<ITiledMapLayer> {
return new LayersIterator(this.map);
}
}

View file

@ -3,7 +3,6 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha
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 {mediaManager} from "../../WebRtc/MediaManager";
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";
@ -12,6 +11,13 @@ 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 { iframeListener } from '../../Api/IframeListener';
import { Subscription } from 'rxjs';
import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
import {get} from "svelte/store";
export const MenuSceneName = 'MenuScene'; export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu'; const gameMenuKey = 'gameMenu';
@ -38,15 +44,41 @@ 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()
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.addMenuOption(menuCommand);
}))
this.subscriptions.add(iframeListener.unregisterMenuCommandStream.subscribe(menuCommand => {
this.destroyMenu(menuCommand);
}))
} }
preload () { reset() {
const addedMenuItems = [...this.menuElement.node.querySelectorAll(".fromApi")];
for (let index = addedMenuItems.length - 1; index >= 0; index--) {
addedMenuItems[index].remove()
}
}
public addMenuOption(menuText: string) {
const wrappingSection = document.createElement("section")
const escapedHtml = HtmlUtils.escapeHtml(menuText);
wrappingSection.innerHTML = `<button class="fromApi" id="${escapedHtml}">${escapedHtml}</button>`
const menuItemContainer = this.menuElement.node.querySelector("#gameMenu main");
if (menuItemContainer) {
menuItemContainer.querySelector(`#${escapedHtml}.fromApi`)?.remove()
menuItemContainer.insertBefore(wrappingSection, menuItemContainer.querySelector("#socialLinks"))
}
}
preload() {
this.load.html(gameMenuKey, 'resources/html/gameMenu.html'); this.load.html(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');
@ -69,11 +101,11 @@ export class MenuScene extends Phaser.Scene {
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();
} }
}); });
@ -131,8 +163,8 @@ export class MenuScene extends Phaser.Scene {
} }
//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,
@ -162,7 +194,7 @@ export class MenuScene extends Phaser.Scene {
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">`;
gameManager.getCurrentGameScene(this).ConsoleGlobalMessageManager.disabledMessageConsole(); consoleGlobalMessageManagerVisibleStore.set(false);
this.tweens.add({ this.tweens.add({
targets: this.menuElement, targets: this.menuElement,
x: closedSideMenuX, x: closedSideMenuX,
@ -182,28 +214,28 @@ 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('select-game-quality') as HTMLInputElement;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-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();
} }
}); });
let middleY = this.scale.height / 2 - 392/2; let middleY = this.scale.height / 2 - 392 / 2;
if(middleY < 0){ if (middleY < 0) {
middleY = 0; middleY = 0;
} }
let middleX = this.scale.width / 2 - 457/2; let middleX = this.scale.width / 2 - 457 / 2;
if(middleX < 0){ if (middleX < 0) {
middleX = 0; middleX = 0;
} }
this.tweens.add({ this.tweens.add({
@ -229,7 +261,7 @@ export class MenuScene extends Phaser.Scene {
} }
private openGameShare(): void{ private openGameShare(): void {
if (this.gameShareOpened) { if (this.gameShareOpened) {
this.closeGameShare(); this.closeGameShare();
return; return;
@ -243,11 +275,11 @@ export class MenuScene extends Phaser.Scene {
this.gameShareOpened = true; this.gameShareOpened = true;
let middleY = this.scale.height / 2 - 85; let middleY = this.scale.height / 2 - 85;
if(middleY < 0){ if (middleY < 0) {
middleY = 0; middleY = 0;
} }
let middleX = this.scale.width / 2 - 200; let middleX = this.scale.width / 2 - 200;
if(middleX < 0){ if (middleX < 0) {
middleX = 0; middleX = 0;
} }
this.tweens.add({ this.tweens.add({
@ -259,7 +291,7 @@ export class MenuScene extends Phaser.Scene {
}); });
} }
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';
@ -272,12 +304,18 @@ export class MenuScene extends Phaser.Scene {
}); });
} }
private onMenuClick(event:MouseEvent) { private onMenuClick(event: MouseEvent) {
if((event?.target as HTMLInputElement).classList.contains('not-button')){ const htmlMenuItem = (event?.target as HTMLInputElement);
if (htmlMenuItem.classList.contains('not-button')) {
return; return;
} }
event.preventDefault(); event.preventDefault();
if (htmlMenuItem.classList.contains("fromApi")) {
sendMenuClickedEvent(htmlMenuItem.id)
return
}
switch ((event?.target as HTMLInputElement).id) { switch ((event?.target as HTMLInputElement).id) {
case 'changeNameButton': case 'changeNameButton':
this.closeSideMenu(); this.closeSideMenu();
@ -307,7 +345,11 @@ export class MenuScene extends Phaser.Scene {
this.toggleFullscreen(); this.toggleFullscreen();
break; break;
case 'adminConsoleButton': case 'adminConsoleButton':
gameManager.getCurrentGameScene(this).ConsoleGlobalMessageManager.activeMessageConsole(); if (get(consoleGlobalMessageManagerVisibleStore)) {
consoleGlobalMessageManagerVisibleStore.set(false);
} else {
consoleGlobalMessageManagerVisibleStore.set(true);
}
break; break;
} }
} }
@ -319,7 +361,7 @@ export class MenuScene extends Phaser.Scene {
gameShareInfo.style.display = 'block'; gameShareInfo.style.display = 'block';
} }
private saveSetting(valueGame: number, valueVideo: number){ private saveSetting(valueGame: number, valueVideo: number) {
if (valueGame !== this.gameQualityValue) { if (valueGame !== this.gameQualityValue) {
this.gameQualityValue = valueGame; this.gameQualityValue = valueGame;
localUserStore.setGameQualityValue(valueGame); localUserStore.setGameQualityValue(valueGame);
@ -340,7 +382,7 @@ export class MenuScene extends Phaser.Scene {
window.open(sparkHost, '_blank'); window.open(sparkHost, '_blank');
} }
private closeAll(){ private closeAll() {
this.closeGameQualityMenu(); this.closeGameQualityMenu();
this.closeGameShare(); this.closeGameShare();
this.gameReportElement.close(); this.gameReportElement.close();
@ -357,6 +399,10 @@ export class MenuScene extends Phaser.Scene {
} }
} }
public destroyMenu(menu: string) {
this.menuElement.getChildByID(menu).remove();
}
public isDirty(): boolean { public isDirty(): boolean {
return false; return false;
} }

View file

@ -1,59 +0,0 @@
export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.MultiPipeline {
// the unique id of this pipeline
public static readonly KEY = 'Outline';
/**
* @param {Phaser.Game} game - the controller of the game instance
*/
constructor(game: Phaser.Game)
{
super({
game: game,
fragShader: `
precision mediump float;
uniform sampler2D uMainSampler;
uniform vec2 uTextureSize;
varying vec2 outTexCoord;
varying float outTintEffect;
varying vec4 outTint;
void main(void)
{
vec4 texture = texture2D(uMainSampler, outTexCoord);
vec4 texel = vec4(outTint.rgb * outTint.a, outTint.a);
vec4 color = texture;
if (outTintEffect == 0.0)
{
color = texture * texel;
}
else if (outTintEffect == 1.0)
{
color.rgb = mix(texture.rgb, outTint.rgb * outTint.a, texture.a);
color.a = texture.a * texel.a;
}
else if (outTintEffect == 2.0)
{
color = texel;
}
vec2 onePixel = vec2(1.0, 1.0) / uTextureSize;
float upAlpha = texture2D(uMainSampler, outTexCoord + vec2(0.0, onePixel.y)).a;
float leftAlpha = texture2D(uMainSampler, outTexCoord + vec2(-onePixel.x, 0.0)).a;
float downAlpha = texture2D(uMainSampler, outTexCoord + vec2(0.0, -onePixel.y)).a;
float rightAlpha = texture2D(uMainSampler, outTexCoord + vec2(onePixel.x, 0.0)).a;
if (texture.a == 0.0 && max(max(upAlpha, downAlpha), max(leftAlpha, rightAlpha)) == 1.0)
{
color = vec4(1.0, 1.0, 0.0, 1.0);
}
gl_FragColor = color;
}
`
});
}
}

View file

@ -2,6 +2,7 @@ import type { Direction } from "../../types";
import type {GameScene} from "../Game/GameScene"; import type {GameScene} from "../Game/GameScene";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {MobileJoystick} from "../Components/MobileJoystick"; import {MobileJoystick} from "../Components/MobileJoystick";
import {enableUserInputsStore} from "../../Stores/UserInputStore";
interface UserInputManagerDatum { interface UserInputManagerDatum {
keyInstance: Phaser.Input.Keyboard.Key; keyInstance: Phaser.Input.Keyboard.Key;
@ -58,6 +59,10 @@ export class UserInputManager {
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
this.initVirtualJoystick(); this.initVirtualJoystick();
} }
enableUserInputsStore.subscribe((enable) => {
enable ? this.restoreControls() : this.disableControls()
})
} }
initVirtualJoystick() { initVirtualJoystick() {

View file

@ -0,0 +1,5 @@
import { writable } from "svelte/store";
export const consoleGlobalMessageManagerVisibleStore = writable(false);
export const consoleGlobalMessageManagerFocusStore = writable(false);

View file

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

View file

@ -1,11 +1,11 @@
import {DivImportance, layoutManager} from "./LayoutManager"; import { DivImportance, layoutManager } from "./LayoutManager";
import {HtmlUtils} from "./HtmlUtils"; import { HtmlUtils } from "./HtmlUtils";
import {discussionManager, SendMessageCallback} from "./DiscussionManager"; 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 { localUserStore } from "../Connexion/LocalUserStore";
import type {UserSimplePeerInterface} from "./SimplePeer"; import type { UserSimplePeerInterface } from "./SimplePeer";
import {SoundMeter} from "../Phaser/Components/SoundMeter"; import { SoundMeter } from "../Phaser/Components/SoundMeter";
import {DISABLE_NOTIFICATIONS} from "../Enum/EnvironmentVariable"; import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
import { import {
localStreamStore, localStreamStore,
} from "../Stores/MediaStore"; } from "../Stores/MediaStore";
@ -14,7 +14,7 @@ import {
} from "../Stores/ScreenSharingStore"; } from "../Stores/ScreenSharingStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore"; import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void; 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;
@ -25,9 +25,11 @@ 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 focused: boolean = true;
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
@ -93,17 +95,17 @@ export class MediaManager {
return `screen-sharing-${userId}`; return `screen-sharing-${userId}`;
} }
disabledMicrophoneByUserId(userId: number){ disabledMicrophoneByUserId(userId: number) {
const element = document.getElementById(`microphone-${userId}`); const element = document.getElementById(`microphone-${userId}`);
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) {
const element = document.getElementById(`microphone-${userId}`); const element = document.getElementById(`microphone-${userId}`);
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'?
@ -120,29 +122,29 @@ export class MediaManager {
} }
} }
enabledVideoByUserId(userId: number){ enabledVideoByUserId(userId: number) {
let element = document.getElementById(`${userId}`); let element = document.getElementById(`${userId}`);
if(element){ if (element) {
element.style.opacity = "1"; element.style.opacity = "1";
} }
element = document.getElementById(`name-${userId}`); element = document.getElementById(`name-${userId}`);
if(element){ if (element) {
element.style.display = "none"; element.style.display = "none";
} }
} }
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 {
console.info("isError", `div-${userId}`); console.info("isError", `div-${userId}`);
const element = document.getElementById(`div-${userId}`); const element = document.getElementById(`div-${userId}`);
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;
} }
@ -153,9 +155,9 @@ export class MediaManager {
} }
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;
@ -166,7 +168,7 @@ export class MediaManager {
this.triggerCloseJistiFrame.set(id, Function); this.triggerCloseJistiFrame.set(id, Function);
} }
public removeTriggerCloseJitsiFrameButton(id: String){ public removeTriggerCloseJitsiFrameButton(id: String) {
this.triggerCloseJistiFrame.delete(id); this.triggerCloseJistiFrame.delete(id);
} }
@ -176,16 +178,16 @@ export class MediaManager {
} }
} }
public addNewMessage(name: string, message: string, isMe: boolean = false){ public addNewMessage(name: string, message: string, isMe: boolean = false) {
discussionManager.addMessage(name, message, isMe); discussionManager.addMessage(name, message, isMe);
//when there are new message, show discussion //when there are new message, show discussion
if(!discussionManager.activatedDiscussion) { if (!discussionManager.activatedDiscussion) {
discussionManager.showDiscussionPart(); discussionManager.showDiscussionPart();
} }
} }
public addSendMessageCallback(userId: string|number, callback: SendMessageCallback){ public addSendMessageCallback(userId: string | number, callback: SendMessageCallback) {
discussionManager.onSendMessageCallback(userId, callback); discussionManager.onSendMessageCallback(userId, callback);
} }

View file

@ -1,4 +1,4 @@
import {registeredCallbacks} from "./Api/iframe/registeredCallbacks"; import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
import { import {
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap,
@ -6,15 +6,16 @@ import {
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";
import sound from "./Api/iframe/sound"; import sound from "./Api/iframe/sound";
import room from "./Api/iframe/room"; import room from "./Api/iframe/room";
import type {ButtonDescriptor} from "./Api/iframe/Ui/ButtonDescriptor"; import player from "./Api/iframe/player";
import type {Popup} from "./Api/iframe/Ui/Popup"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type {Sound} from "./Api/iframe/Sound/Sound"; import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound";
const wa = { const wa = {
ui, ui,
@ -23,6 +24,7 @@ const wa = {
chat, chat,
sound, sound,
room, room,
player,
// All methods below are deprecated and should not be used anymore. // All methods below are deprecated and should not be used anymore.
// They are kept here for backward compatibility. // They are kept here for backward compatibility.
@ -77,7 +79,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);
}, },
@ -85,7 +87,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);
}, },
@ -101,7 +103,7 @@ const wa = {
/** /**
* @deprecated Use WA.nav.openCoWebSite instead * @deprecated Use WA.nav.openCoWebSite instead
*/ */
openCoWebSite(url : string) : void{ openCoWebSite(url: string): void {
console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead'); console.warn('Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead');
nav.openCoWebSite(url); nav.openCoWebSite(url);
}, },

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