Merge remote-tracking branch 'remotes/upstream/develop' into tiles-start-positions

This commit is contained in:
jonny 2021-06-25 18:14:40 +02:00
commit 7f61e9addd
182 changed files with 17118 additions and 4494 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
working-directory: "front"
# We will enable prettier checks on front in a few month, when most PRs without prettier have been merged
# - name: "Prettier"
# run: yarn run pretty-check
# working-directory: "front"
continuous-integration-pusher:
name: "Continuous Integration Pusher"
@ -107,6 +112,10 @@ jobs:
run: yarn test
working-directory: "pusher"
- name: "Prettier"
run: yarn run pretty-check
working-directory: "pusher"
continuous-integration-back:
name: "Continuous Integration Back"
@ -150,3 +159,7 @@ jobs:
run: yarn test
working-directory: "back"
- name: "Prettier"
run: yarn run pretty-check
working-directory: "back"

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

@ -7,6 +7,14 @@
- 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
@ -50,6 +58,7 @@
- New scripting API features:
- Use `WA.loadSound(): Sound` to load / play / stop a sound
### Bug Fixes
- 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",
"profile": "tsc && node --prof ./dist/server.js",
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts"
"lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts",
"fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts",
"precommit": "lint-staged",
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
},
"repository": {
"type": "git",
@ -66,7 +69,14 @@
"@typescript-eslint/parser": "^2.26.0",
"eslint": "^6.8.0",
"jasmine": "^3.5.0",
"lint-staged": "^11.0.0",
"prettier": "^2.3.1",
"ts-node-dev": "^1.0.0-pre.44",
"typescript": "^3.8.3"
},
"lint-staged": {
"*.ts": [
"prettier --write"
]
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
*/
export interface Movable {
getPosition(): PositionInterface
getPosition(): PositionInterface;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { App as _App, AppOptions } from 'uWebSockets.js';
import BaseApp from './baseapp';
import { extend } from './utils';
import { UwsApp } from './types';
import { App as _App, AppOptions } from "uWebSockets.js";
import BaseApp from "./baseapp";
import { extend } from "./utils";
import { UwsApp } from "./types";
class App extends (<UwsApp>_App) {
constructor(options: AppOptions = {}) {

View File

@ -1,18 +1,18 @@
import { Readable } from 'stream';
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
import { Readable } from "stream";
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
import formData from './formdata';
import { stob } from './utils';
import { Handler } from './types';
import {join} from "path";
import formData from "./formdata";
import { stob } from "./utils";
import { Handler } from "./types";
import { join } from "path";
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
const noOp = () => true;
const handleBody = (res: HttpResponse, req: HttpRequest) => {
const contType = req.getHeader('content-type');
const contType = req.getHeader("content-type");
res.bodyStream = function() {
res.bodyStream = function () {
const stream = new Readable();
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
@ -29,24 +29,21 @@ const handleBody = (res: HttpResponse, req: HttpRequest) => {
res.body = () => stob(res.bodyStream());
if (contType.includes('application/json'))
res.json = async () => JSON.parse(await res.body());
if (contTypes.map(t => contType.includes(t)).includes(true))
res.formData = formData.bind(res, contType);
if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
};
class BaseApp {
_sockets = new Map();
ws!: TemplatedApp['ws'];
get!: TemplatedApp['get'];
_post!: TemplatedApp['post'];
_put!: TemplatedApp['put'];
_patch!: TemplatedApp['patch'];
_listen!: TemplatedApp['listen'];
ws!: TemplatedApp["ws"];
get!: TemplatedApp["get"];
_post!: TemplatedApp["post"];
_put!: TemplatedApp["put"];
_patch!: TemplatedApp["patch"];
_listen!: TemplatedApp["listen"];
post(pattern: string, handler: Handler) {
if (typeof handler !== 'function')
throw Error(`handler should be a function, given ${typeof handler}.`);
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
this._post(pattern, (res, req) => {
handleBody(res, req);
handler(res, req);
@ -55,8 +52,7 @@ class BaseApp {
}
put(pattern: string, handler: Handler) {
if (typeof handler !== 'function')
throw Error(`handler should be a function, given ${typeof handler}.`);
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
this._put(pattern, (res, req) => {
handleBody(res, req);
@ -66,8 +62,7 @@ class BaseApp {
}
patch(pattern: string, handler: Handler) {
if (typeof handler !== 'function')
throw Error(`handler should be a function, given ${typeof handler}.`);
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
this._patch(pattern, (res, req) => {
handleBody(res, req);
@ -77,23 +72,21 @@ class BaseApp {
}
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
if (typeof p === 'number' && typeof h === 'string') {
this._listen(h, p, socket => {
if (typeof p === "number" && typeof h === "string") {
this._listen(h, p, (socket) => {
this._sockets.set(p, socket);
if (cb === undefined) {
throw new Error('cb undefined');
throw new Error("cb undefined");
}
cb(socket);
});
} else if (typeof h === 'number' && typeof p === 'function') {
this._listen(h, 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)'
);
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
}
return this;
@ -104,7 +97,7 @@ class BaseApp {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
this._sockets.delete(port);
} else {
this._sockets.forEach(app => {
this._sockets.forEach((app) => {
us_listen_socket_close(app);
});
this._sockets.clear();

View File

@ -1,7 +1,7 @@
import { createWriteStream } from 'fs';
import { join, dirname } from 'path';
import Busboy from 'busboy';
import mkdirp from 'mkdirp';
import { createWriteStream } from "fs";
import { join, dirname } from "path";
import Busboy from "busboy";
import mkdirp from "mkdirp";
function formData(
contType: string,
@ -19,9 +19,9 @@ function formData(
filename?: (oldName: string) => string;
} = {}
) {
console.log('Enter form data');
console.log("Enter form data");
options.headers = {
'content-type': contType
"content-type": contType,
};
return new Promise((resolve, reject) => {
@ -30,47 +30,46 @@ function formData(
this.bodyStream().pipe(busb);
busb.on('limit', () => {
busb.on("limit", () => {
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 } = {
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
filePath: undefined,
};
if (typeof options.tmpDir === 'string') {
if (typeof options.filename === 'function') filename = options.filename(filename);
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;
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);
busb.on("field", function (fieldname, value) {
if (typeof options.onField === "function") options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on('finish', function() {
busb.on("finish", function () {
resolve(ret);
});
busb.on('error', reject);
busb.on("error", reject);
});
}
@ -79,7 +78,7 @@ function setRetValue(
fieldname: string,
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);
if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);

View File

@ -1,7 +1,7 @@
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js';
import BaseApp from './baseapp';
import { extend } from './utils';
import { UwsApp } from './types';
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
import BaseApp from "./baseapp";
import { extend } from "./utils";
import { UwsApp } from "./types";
class SSLApp extends (<UwsApp>_SSLApp) {
constructor(options: AppOptions) {

View File

@ -1,4 +1,4 @@
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
export type UwsApp = {
(options: AppOptions): TemplatedApp;

View File

@ -1,25 +1,24 @@
import { ReadStream } from 'fs';
import { ReadStream } from "fs";
function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(
Object.keys(from)
);
ownProps.forEach(prop => {
if (prop === 'constructor' || from[prop] === undefined) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extend(who: any, from: any, overwrite = true) {
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
ownProps.forEach((prop) => {
if (prop === "constructor" || from[prop] === undefined) return;
if (who[prop] && overwrite) {
who[`_${prop}`] = who[prop];
}
if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who);
if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
else who[prop] = from[prop];
});
}
function stob(stream: ReadStream): Promise<Buffer> {
return new Promise(resolve => {
return new Promise((resolve) => {
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) {
case 0:
resolve(Buffer.allocUnsafe(0));

View File

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

View File

@ -1,3 +1,3 @@
export const arrayIntersect = (array1: string[], array2: string[]) : boolean => {
return array1.filter(value => array2.includes(value)).length > 0;
}
export const arrayIntersect = (array1: string[], array2: string[]): boolean => {
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 clientLeaveEvent = 'clientLeave';
const clientJoinEvent = "clientJoin";
const clientLeaveEvent = "clientLeave";
class ClientEventsEmitter extends EventEmitter {
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)) {
return secNSec[0] * 1000 + secNSec[1] / 1000000;
}
@ -12,17 +12,17 @@ class CpuTracker {
private overHeating: boolean = false;
constructor() {
let time = process.hrtime.bigint()
let usage = process.cpuUsage()
let time = process.hrtime.bigint();
let usage = process.cpuUsage();
setInterval(() => {
const elapTime = process.hrtime.bigint();
const elapUsage = process.cpuUsage(usage)
usage = process.cpuUsage()
const elapUsage = process.cpuUsage(usage);
usage = process.cpuUsage();
const elapTimeMS = elapTime - time;
const elapUserMS = secNSec2ms(elapUsage.user)
const elapSystMS = secNSec2ms(elapUsage.system)
this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000)
const elapUserMS = secNSec2ms(elapUsage.user);
const elapSystMS = secNSec2ms(elapUsage.system);
this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
time = elapTime;

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
class GaugeManager {
@ -10,29 +10,29 @@ class GaugeManager {
constructor() {
this.nbRoomsGauge = new Gauge({
name: 'workadventure_nb_rooms',
help: 'Number of active rooms'
name: "workadventure_nb_rooms",
help: "Number of active rooms",
});
this.nbClientsGauge = new Gauge({
name: 'workadventure_nb_sockets',
help: 'Number of connected sockets',
labelNames: [ ]
name: "workadventure_nb_sockets",
help: "Number of connected sockets",
labelNames: [],
});
this.nbClientsPerRoomGauge = new Gauge({
name: 'workadventure_nb_clients_per_room',
help: 'Number of clients per room',
labelNames: [ 'room' ]
name: "workadventure_nb_clients_per_room",
help: "Number of clients per room",
labelNames: ["room"],
});
this.nbGroupsPerRoomCounter = new Counter({
name: 'workadventure_counter_groups_per_room',
help: 'Counter of groups per room',
labelNames: [ 'room' ]
name: "workadventure_counter_groups_per_room",
help: "Counter of groups per room",
labelNames: ["room"],
});
this.nbGroupsPerRoomGauge = new Gauge({
name: 'workadventure_nb_groups_per_room',
help: 'Number of groups per room',
labelNames: [ 'room' ]
name: "workadventure_nb_groups_per_room",
help: "Number of groups per room",
labelNames: ["room"],
});
}
@ -54,12 +54,12 @@ class GaugeManager {
}
incNbGroupsPerRoomGauge(roomId: string): void {
this.nbGroupsPerRoomCounter.inc({ room: roomId })
this.nbGroupsPerRoomGauge.inc({ room: roomId })
this.nbGroupsPerRoomCounter.inc({ room: roomId });
this.nbGroupsPerRoomGauge.inc({ room: roomId });
}
decNbGroupsPerRoomGauge(roomId: string): void {
this.nbGroupsPerRoomGauge.dec({ room: roomId })
this.nbGroupsPerRoomGauge.dec({ room: roomId });
}
}

View File

@ -1,5 +1,5 @@
import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb";
import {UserSocket} from "_Model/User";
import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb";
import { UserSocket } from "_Model/User";
export function emitError(Client: UserSocket, message: string): void {
const errorMessage = new ErrorMessage();

View File

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

View File

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

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,6 +4,7 @@
- [Navigation functions](api-nav.md)
- [Chat functions](api-chat.md)
- [Room functions](api-room.md)
- [Player functions](api-player.md)
- [UI functions](api-ui.md)
- [Sound functions](api-sound.md)
- [Controls functions](api-controls.md)

View File

@ -1,6 +1,22 @@
{.section-title.accent.text-primary}
# API Room functions Reference
### Working with group layers
If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together.
Example :
<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
```
@ -31,3 +47,68 @@ WA.room.onLeaveZone('myZone', () => {
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
})
```
### Show / Hide a layer
```
WA.room.showLayer(layerName : string): void
WA.room.hideLayer(layerName : string) : void
```
These 2 methods can be used to show and hide a layer.
Example :
```javascript
WA.room.showLayer('bottom');
//...
WA.room.hideLayer('bottom');
```
### Set/Create properties in a layer
```
WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void;
```
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
Example :
```javascript
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
```
### Getting information on the current room
```
WA.room.getCurrentRoom(): Promise<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

@ -65,3 +65,25 @@ WA.room.onLeaveZone('myZone', () => {
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
...
<script>
WA.sendChatMessage('Hello world', 'Mr Robot');
WA.chat.sendChatMessage('Hello world', 'Mr Robot');
</script>
...
```

View File

@ -61,20 +61,22 @@ By default, the characters can traverse any tiles. If you want to prevent your c
To make a tile "collidable", you should:
1. select the relevant tileset and switch to "edit" mode:
{.document-img}
![](https://workadventu.re/img/docs/collides-1.png)
![](https://workadventu.re/img/docs/collides-1.png){.document-img}
2. right click on a tile of the tileset to select it:
{.document-img}
![](https://workadventu.re/img/docs/collides-2.png)
![](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":
{.document-img}
![](https://workadventu.re/img/docs/collides-3.png)
![](https://workadventu.re/img/docs/collides-3.png){.document-img}
Please add a `collides` property. The type of the property must be **bool**.
4. finally, check the checkbox for the `collides` property:
{.document-img}
![](https://workadventu.re/img/docs/collides-4.png)
![](https://workadventu.re/img/docs/collides-4.png){.document-img}
Repeat for every tile that should be "collidable".

6
front/.gitignore vendored
View File

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

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
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

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",
"html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0",
"lint-staged": "^11.0.0",
"mini-css-extract-plugin": "^1.6.0",
"node-polyfill-webpack-plugin": "^1.1.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.1",
"sass": "^1.32.12",
"sass-loader": "^11.1.0",
"svelte": "^3.38.2",
@ -61,7 +63,15 @@
"test": "cross-env TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch",
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\""
"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": "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

@ -0,0 +1,12 @@
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,16 @@
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,17 @@
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,57 +1,71 @@
import type { ButtonClickedEvent } from './ButtonClickedEvent';
import type { ChatEvent } from './ChatEvent';
import type { ClosePopupEvent } from './ClosePopupEvent';
import type { EnterLeaveEvent } from './EnterLeaveEvent';
import type { GoToPageEvent } from './GoToPageEvent';
import type { LoadPageEvent } from './LoadPageEvent';
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
import type { OpenPopupEvent } from './OpenPopupEvent';
import type { OpenTabEvent } from './OpenTabEvent';
import type { UserInputChatEvent } from './UserInputChatEvent';
import type { LoadSoundEvent} from "./LoadSoundEvent";
import type {PlaySoundEvent} from "./PlaySoundEvent";
import type { GameStateEvent } from "./GameStateEvent";
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent";
import type { ClosePopupEvent } from "./ClosePopupEvent";
import type { EnterLeaveEvent } from "./EnterLeaveEvent";
import type { GoToPageEvent } from "./GoToPageEvent";
import type { LoadPageEvent } from "./LoadPageEvent";
import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenTabEvent } from "./OpenTabEvent";
import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { DataLayerEvent } from "./DataLayerEvent";
import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent";
import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T
data: T;
}
export type IframeEventMap = {
//getState: GameStateEvent,
// updateTile: UpdateTileEvent
loadPage: LoadPageEvent
chat: ChatEvent,
openPopup: OpenPopupEvent
closePopup: ClosePopupEvent
openTab: OpenTabEvent
goToPage: GoToPageEvent
openCoWebSite: OpenCoWebSiteEvent
closeCoWebSite: null
disablePlayerControls: null
restorePlayerControls: null
displayBubble: null
removeBubble: null
loadSound: LoadSoundEvent
playSound: PlaySoundEvent
stopSound: null,
}
loadPage: LoadPageEvent;
chat: ChatEvent;
openPopup: OpenPopupEvent;
closePopup: ClosePopupEvent;
openTab: OpenTabEvent;
goToPage: GoToPageEvent;
openCoWebSite: OpenCoWebSiteEvent;
closeCoWebSite: null;
disablePlayerControls: null;
restorePlayerControls: null;
displayBubble: null;
removeBubble: null;
onPlayerMove: undefined;
showLayer: LayerEvent;
hideLayer: LayerEvent;
setProperty: SetPropertyEvent;
getDataLayer: undefined;
loadSound: LoadSoundEvent;
playSound: PlaySoundEvent;
stopSound: null;
getState: undefined;
registerMenuCommand: MenuItemRegisterEvent;
};
export interface IframeEvent<T extends keyof IframeEventMap> {
type: T;
data: IframeEventMap[T];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string';
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
typeof event.type === "string";
export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent
enterEvent: EnterLeaveEvent
leaveEvent: EnterLeaveEvent
buttonClickedEvent: ButtonClickedEvent
// gameState: GameStateEvent
userInputChat: UserInputChatEvent;
enterEvent: EnterLeaveEvent;
leaveEvent: EnterLeaveEvent;
buttonClickedEvent: ButtonClickedEvent;
gameState: GameStateEvent;
hasPlayerMoved: HasPlayerMovedEvent;
dataLayer: DataLayerEvent;
menuItemClicked: MenuItemClickedEvent;
}
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T;
@ -59,4 +73,6 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string';
export const isIframeResponseEventWrapper = (event: {
type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";

View File

@ -0,0 +1,11 @@
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

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

View File

@ -0,0 +1,13 @@
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,11 @@
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,26 @@
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,4 +1,3 @@
import { Subject } from "rxjs";
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import { HtmlUtils } from "../WebRtc/HtmlUtils";
@ -10,12 +9,28 @@ import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
import { scriptUtils } from "./ScriptUtils";
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent";
import {
IframeEvent,
IframeEventMap,
IframeResponseEvent,
IframeResponseEventMap,
isIframeEventWrapper,
TypedMessageEvent,
} from "./Events/IframeEvent";
import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
import { isLoadPageEvent } from './Events/LoadPageEvent';
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
//import { isLoadPageEvent } from './Events/LoadPageEvent';
import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import { isMenuItemRegisterEvent } from "./Events/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.
* Also allows to send messages to those iframes.
@ -33,7 +48,6 @@ class IframeListener {
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
public readonly goToPageStream = this._goToPageStream.asObservable();
private readonly _loadPageStream: Subject<string> = new Subject();
public readonly loadPageStream = this._loadPageStream.asObservable();
@ -58,6 +72,27 @@ class IframeListener {
private readonly _removeBubbleStream: Subject<void> = new Subject();
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();
public readonly playSoundStream = this._playSoundStream.asObservable();
@ -68,21 +103,21 @@ class IframeListener {
public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false;
init() {
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
window.addEventListener(
"message",
(message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
// Do we trust the sender of this message?
// Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined;
foundSrc = [...this.scripts.keys()].find(key => {
return this.scripts.get(key)?.contentWindow == message.source
});
if (foundSrc === undefined) {
for (const iframe of this.iframes) {
let iframe: HTMLIFrameElement;
for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
@ -92,59 +127,77 @@ class IframeListener {
if (foundSrc === undefined) {
return;
}
}
const payload = message.data;
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);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
}
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
}
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
}
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
}
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
}
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
}
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
}
else if (payload.type === 'closeCoWebSite') {
scriptUtils.closeCoWebSite();
}
else if (payload.type === 'disablePlayerControls') {
this._disablePlayerControlStream.next();
}
else if (payload.type === 'restorePlayerControls') {
this._enablePlayerControlStream.next();
}
else if (payload.type === 'displayBubble') {
this._displayBubbleStream.next();
}
else if (payload.type === 'removeBubble') {
this._removeBubbleStream.next();
}else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url);
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
} else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
} else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
} else if (payload.type === "closeCoWebSite") {
scriptUtils.closeCoWebSite();
} else if (payload.type === "disablePlayerControls") {
this._disablePlayerControlStream.next();
} else if (payload.type === "restorePlayerControls") {
this._enablePlayerControlStream.next();
} else if (payload.type === "displayBubble") {
this._displayBubbleStream.next();
} else if (payload.type === "removeBubble") {
this._removeBubbleStream.next();
} else if (payload.type == "getState") {
this._gameStateStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
this.iframeCloseCallbacks.get(iframe).push(() => {
this._unregisterMenuCommandStream.next(data);
});
handleMenuItemRegistrationEvent(payload.data);
}
}
},
false
);
}
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({
type: "dataLayer",
data: dataLayerEvent,
});
}
}, false);
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
type: "gameState",
data: gameStateEvent,
});
}
/**
@ -152,25 +205,29 @@ class IframeListener {
*/
registerIframe(iframe: HTMLIFrameElement): void {
this.iframes.add(iframe);
this.iframeCloseCallbacks.set(iframe, []);
}
unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframeCloseCallbacks.get(iframe)?.forEach((callback) => {
callback();
});
this.iframes.delete(iframe);
}
registerScript(scriptUrl: string): void {
console.log('Loading map related script at ', scriptUrl)
console.log("Loading map related script at ", scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode (
const iframe = document.createElement('iframe');
iframe.id = this.getIFrameId(scriptUrl);
iframe.style.display = 'none';
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = "none";
iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
iframe.sandbox.add("allow-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation");
document.body.prepend(iframe);
@ -178,41 +235,45 @@ class IframeListener {
this.registerIframe(iframe);
} else {
// production code
const iframe = document.createElement('iframe');
iframe.id = this.getIFrameId(scriptUrl);
iframe.style.display = 'none';
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = "none";
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
const html = '<!doctype html>\n' +
'\n' +
'<html lang="en">\n' +
'<head>\n' +
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
'<script src="' + scriptUrl + '" ></script>\n' +
'</head>\n' +
'</html>\n';
iframe.sandbox.add("allow-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation");
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = html;
iframe.srcdoc =
"<!doctype html>\n" +
"\n" +
'<html lang="en">\n' +
"<head>\n" +
'<script src="' +
window.location.protocol +
"//" +
window.location.host +
'/iframe_api.js" ></script>\n' +
'<script src="' +
scriptUrl +
'" ></script>\n' +
"<title></title>\n" +
"</head>\n" +
"</html>\n";
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
}
}
private getIFrameId(scriptUrl: string): string {
return 'script' + btoa(scriptUrl);
private static getIFrameId(scriptUrl: string): string {
return "script" + btoa(scriptUrl);
}
unregisterScript(scriptUrl: string): void {
const iFrameId = this.getIFrameId(scriptUrl);
const iFrameId = IframeListener.getIFrameId(scriptUrl);
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
if (!iframe) {
throw new Error('Unknown iframe for script "' + scriptUrl + '"');
@ -225,50 +286,58 @@ class IframeListener {
sendUserInputChat(message: string) {
this.postMessage({
'type': 'userInputChat',
'data': {
'message': message,
} as UserInputChatEvent
type: "userInputChat",
data: {
message: message,
} as UserInputChatEvent,
});
}
sendEnterEvent(name: string) {
this.postMessage({
'type': 'enterEvent',
'data': {
"name": name
} as EnterLeaveEvent
type: "enterEvent",
data: {
name: name,
} as EnterLeaveEvent,
});
}
sendLeaveEvent(name: string) {
this.postMessage({
'type': 'leaveEvent',
'data': {
"name": name
} as EnterLeaveEvent
type: "leaveEvent",
data: {
name: name,
} as EnterLeaveEvent,
});
}
hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) {
this.postMessage({
type: "hasPlayerMoved",
data: event,
});
}
}
sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({
'type': 'buttonClickedEvent',
'data': {
type: "buttonClickedEvent",
data: {
popupId,
buttonId
} as ButtonClickedEvent
buttonId,
} as ButtonClickedEvent,
});
}
/**
* 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) {
iframe.contentWindow?.postMessage(message, '*');
iframe.contentWindow?.postMessage(message, "*");
}
}
}
export const iframeListener = new IframeListener();

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

@ -1,30 +1,30 @@
import type { ChatEvent } from '../Events/ChatEvent'
import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent'
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'
import type { ChatEvent } from "../Events/ChatEvent";
import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import {Subject} from "rxjs";
import { Subject } from "rxjs";
const chatStream = new Subject<string>();
class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
callbacks = [apiCallback({
callbacks = [
apiCallback({
callback: (event: UserInputChatEvent) => {
chatStream.next(event.message);
},
type: "userInputChat",
typeChecker: isUserInputChatEvent
})]
typeChecker: isUserInputChatEvent,
}),
];
sendChatMessage(message: string, author: string) {
sendToWorkadventure({
type: 'chat',
type: "chat",
data: {
'message': message,
'author': author
} as ChatEvent
})
message: message,
author: author,
},
});
}
/**
@ -35,4 +35,4 @@ class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatC
}
}
export default new WorkadventureChatCommands()
export default new WorkadventureChatCommands();

View File

@ -1,56 +1,54 @@
import type { GoToPageEvent } from '../Events/GoToPageEvent';
import type { OpenTabEvent } from '../Events/OpenTabEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent";
import type { GoToPageEvent } from "../Events/GoToPageEvent";
import type { OpenTabEvent } from "../Events/OpenTabEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import type { OpenCoWebSiteEvent } from "../Events/OpenCoWebSiteEvent";
import type { LoadPageEvent } from "../Events/LoadPageEvent";
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
callbacks = []
callbacks = [];
openTab(url: string): void {
sendToWorkadventure({
"type": 'openTab',
"data": {
url
} as OpenTabEvent
type: "openTab",
data: {
url,
},
});
}
goToPage(url: string): void {
sendToWorkadventure({
"type": 'goToPage',
"data": {
url
} as GoToPageEvent
type: "goToPage",
data: {
url,
},
});
}
goToRoom(url: string): void {
sendToWorkadventure({
"type": 'loadPage',
"data": {
url
}
type: "loadPage",
data: {
url,
},
});
}
openCoWebSite(url: string): void {
sendToWorkadventure({
"type": 'openCoWebSite',
"data": {
url
} as OpenCoWebSiteEvent
type: "openCoWebSite",
data: {
url,
},
});
}
closeCoWebSite(): void {
sendToWorkadventure({
"type": 'closeCoWebSite',
data: null
type: "closeCoWebSite",
data: null,
});
}
}
export default new WorkadventureNavigationCommands();

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,52 @@
import { Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent';
import { IframeApiContribution } from './IframeApiContribution';
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import type { LayerEvent } from "../Events/LayerEvent";
import type { SetPropertyEvent } from "../Events/setPropertyEvent";
import type { GameStateEvent } from "../Events/GameStateEvent";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { DataLayerEvent } from "../Events/DataLayerEvent";
import { isGameStateEvent } from "../Events/GameStateEvent";
import { isDataLayerEvent } from "../Events/DataLayerEvent";
const enterStreams: Map<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> {
callbacks = [
@ -13,18 +55,30 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
enterStreams.get(payloadData.name)?.next();
},
type: "enterEvent",
typeChecker: isEnterLeaveEvent
typeChecker: isEnterLeaveEvent,
}),
apiCallback({
type: "leaveEvent",
typeChecker: isEnterLeaveEvent,
callback: (payloadData) => {
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);
},
}),
];
onEnterZone(name: string, callback: () => void): void {
let subject = enterStreams.get(name);
@ -33,7 +87,6 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
enterStreams.set(name, subject);
}
subject.subscribe(callback);
}
onLeaveZone(name: string, callback: () => void): void {
let subject = leaveStreams.get(name);
@ -43,8 +96,39 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
}
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 };
});
}
}
export default new WorkadventureRoomCommands();

View File

@ -1,26 +1,31 @@
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
import type { ClosePopupEvent } from '../Events/ClosePopupEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import {Popup} from "./Ui/Popup";
import type {ButtonClickedCallback, ButtonDescriptor} from "./Ui/ButtonDescriptor";
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import { Popup } from "./Ui/Popup";
let popupId = 0;
const popups: Map<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 {
zone: string
objectLayerName?: string,
popupText: string,
delay?: number
popupOptions: Array<ButtonDescriptor>
zone: string;
objectLayerName?: string;
popupText: string;
delay?: number;
popupOptions: Array<ButtonDescriptor>;
}
class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
callbacks = [apiCallback({
callbacks = [
apiCallback({
type: "buttonClickedEvent",
typeChecker: isButtonClickedEvent,
callback: (payloadData) => {
@ -32,9 +37,19 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
if (callback) {
callback(popup);
}
},
}),
apiCallback({
type: "menuItemClicked",
typeChecker: isMenuItemClickedEvent,
callback: (event) => {
const callback = menuCallbacks.get(event.menuItem);
if (callback) {
callback(event.menuItem);
}
})];
},
}),
];
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
popupId++;
@ -53,30 +68,40 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
}
sendToWorkadventure({
'type': 'openPopup',
'data': {
type: "openPopup",
data: {
popupId,
targetObject,
message,
buttons: buttons.map((button) => {
return {
label: button.label,
className: button.className
className: button.className,
};
})
}
}),
},
});
popups.set(popupId, popup)
popups.set(popupId, popup);
return popup;
}
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({
type: "registerMenuCommand",
data: {
menutItem: commandDescriptor,
},
});
}
displayBubble(): void {
sendToWorkadventure({ 'type': 'displayBubble', data: null });
sendToWorkadventure({ type: "displayBubble", data: null });
}
removeBubble(): void {
sendToWorkadventure({ 'type': 'removeBubble', data: null });
sendToWorkadventure({ type: "removeBubble", data: null });
}
}

View File

@ -1,5 +1,5 @@
<script lang="typescript">
import {enableCameraSceneVisibilityStore, gameOverlayVisibilityStore} from "../Stores/MediaStore";
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore";
import CameraControls from "./CameraControls.svelte";
import MyCamera from "./MyCamera.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
@ -21,10 +21,13 @@
import AudioPlaying from "./UI/AudioPlaying.svelte";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import ErrorDialog from "./UI/ErrorDialog.svelte";
import VideoOverlay from "./Video/VideoOverlay.svelte";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
export let game: Game;
</script>
<div>
@ -68,6 +71,7 @@
-->
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay></VideoOverlay>
<MyCamera></MyCamera>
<CameraControls></CameraControls>
</div>

View File

@ -7,6 +7,11 @@
import cinemaCloseImg from "./images/cinema-close.svg";
import microphoneImg from "./images/microphone.svg";
import microphoneCloseImg from "./images/microphone-close.svg";
import layoutPresentationImg from "./images/layout-presentation.svg";
import layoutChatImg from "./images/layout-chat.svg";
import {layoutModeStore} from "../Stores/StreamableCollectionStore";
import {LayoutMode} from "../WebRtc/LayoutManager";
import {peerStore} from "../Stores/PeerStore";
function screenSharingClick(): void {
if ($requestedScreenSharingState === true) {
@ -32,10 +37,24 @@
}
}
function switchLayoutMode() {
if ($layoutModeStore === LayoutMode.Presentation) {
$layoutModeStore = LayoutMode.VideoChat;
} else {
$layoutModeStore = LayoutMode.Presentation;
}
}
</script>
<div>
<div class="btn-cam-action">
<div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
{#if $layoutModeStore === LayoutMode.Presentation }
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode">
{:else}
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
{/if}
</div>
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState}
<img src={monitorImg} alt="Start screen sharing">

View File

@ -58,7 +58,7 @@
<div class="horizontal-sound-meter" class:active={display}>
{#each [...Array(NB_BARS).keys()] as i}
{#each [...Array(NB_BARS).keys()] as i (i)}
<div style={color(i, volume)}></div>
{/each}
</div>

View File

@ -6,8 +6,6 @@
export let stream: MediaStream|null;
let volume = 0;
const NB_BARS = 5;
let timeout: ReturnType<typeof setTimeout>;
const soundMeter = new SoundMeter();
let display = false;
@ -23,7 +21,7 @@
timeout = setInterval(() => {
try{
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
volume = soundMeter.getVolume();
//console.log(volume);
}catch(err){
@ -45,9 +43,9 @@
<div class="sound-progress" class:active={display}>
<span class:active={volume > 1}></span>
<span class:active={volume > 2}></span>
<span class:active={volume > 3}></span>
<span class:active={volume > 4}></span>
<span class:active={volume > 5}></span>
<span class:active={volume > 10}></span>
<span class:active={volume > 15}></span>
<span class:active={volume > 40}></span>
<span class:active={volume > 70}></span>
</div>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
import {afterUpdate, onDestroy} from "svelte";
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
import MediaBox from "./MediaBox.svelte";
let cssClass = 'one-col';
const unsubscribe = streamableCollectionStore.subscribe((displayableMedias) => {
const nbUsers = displayableMedias.size;
if (nbUsers <= 1) {
cssClass = 'one-col';
} else if (nbUsers <= 4) {
cssClass = 'two-col';
} else if (nbUsers <= 9) {
cssClass = 'three-col';
} else {
cssClass = 'four-col';
}
});
onDestroy(() => {
unsubscribe();
});
afterUpdate(() => {
biggestAvailableAreaStore.recompute();
})
</script>
<div class="chat-mode {cssClass}">
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
<MediaBox streamable={peer}></MediaBox>
{/each}
</div>

View File

@ -0,0 +1,16 @@
<script lang="typescript">
import type {ScreenSharingLocalMedia} from "../../Stores/ScreenSharingStore";
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {srcObject} from "./utils";
export let peer : ScreenSharingLocalMedia;
let stream = peer.stream;
export let cssClass : string|undefined;
</script>
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
{#if stream}
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
{/if}
</div>

View File

@ -0,0 +1,20 @@
<script lang="ts">
import {VideoPeer} from "../../WebRtc/VideoPeer";
import VideoMediaBox from "./VideoMediaBox.svelte";
import ScreenSharingMediaBox from "./ScreenSharingMediaBox.svelte";
import {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
import LocalStreamMediaBox from "./LocalStreamMediaBox.svelte";
import type {Streamable} from "../../Stores/StreamableCollectionStore";
export let streamable: Streamable;
</script>
<div class="media-container">
{#if streamable instanceof VideoPeer}
<VideoMediaBox peer={streamable}/>
{:else if streamable instanceof ScreenSharingPeer}
<ScreenSharingMediaBox peer={streamable}/>
{:else}
<LocalStreamMediaBox peer={streamable} cssClass=""/>
{/if}
</div>

View File

@ -0,0 +1,24 @@
<script lang="ts">
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {afterUpdate} from "svelte";
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
import MediaBox from "./MediaBox.svelte";
afterUpdate(() => {
biggestAvailableAreaStore.recompute();
})
</script>
<div class="main-section">
{#if $videoFocusStore }
<MediaBox streamable={$videoFocusStore}></MediaBox>
{/if}
</div>
<aside class="sidebar">
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
{#if peer !== $videoFocusStore }
<MediaBox streamable={peer}></MediaBox>
{/if}
{/each}
</aside>

View File

@ -0,0 +1,33 @@
<script lang="ts">
import type {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {getColorByString, srcObject} from "./utils";
export let peer: ScreenSharingPeer;
let streamStore = peer.streamStore;
let name = peer.userName;
let statusStore = peer.statusStore;
</script>
<div class="video-container">
{#if $statusStore === 'connecting'}
<div class="connecting-spinner"></div>
{/if}
{#if $statusStore === 'error'}
<div class="rtc-error"></div>
{/if}
{#if $streamStore === null}
<i style="background-color: {getColorByString(name)};">{name}</i>
{:else}
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
{/if}
</div>
<style lang="scss">
.video-container {
video {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,48 @@
<script lang="ts">
import type {VideoPeer} from "../../WebRtc/VideoPeer";
import SoundMeterWidget from "../SoundMeterWidget.svelte";
import microphoneCloseImg from "../images/microphone-close.svg";
import reportImg from "./images/report.svg";
import blockSignImg from "./images/blockSign.svg";
import {videoFocusStore} from "../../Stores/VideoFocusStore";
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
import {getColorByString, srcObject} from "./utils";
export let peer: VideoPeer;
let streamStore = peer.streamStore;
let name = peer.userName;
let statusStore = peer.statusStore;
let constraintStore = peer.constraintsStore;
function openReport(peer: VideoPeer): void {
showReportScreenStore.set({ userId:peer.userId, userName: peer.userName });
}
</script>
<div class="video-container">
{#if $statusStore === 'connecting'}
<div class="connecting-spinner"></div>
{/if}
{#if $statusStore === 'error'}
<div class="rtc-error"></div>
{/if}
{#if !$constraintStore || $constraintStore.video === false}
<i style="background-color: {getColorByString(name)};">{name}</i>
{/if}
{#if $constraintStore && $constraintStore.audio === false}
<img src={microphoneCloseImg} alt="Muted">
{/if}
<button class="report" on:click={() => openReport(peer)}>
<img alt="Report this user" src={reportImg}>
<span>Report/Block</span>
</button>
{#if $streamStore }
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
{/if}
<img src={blockSignImg} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
{/if}
</div>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import {LayoutMode} from "../../WebRtc/LayoutManager";
import {layoutModeStore} from "../../Stores/StreamableCollectionStore";
import PresentationLayout from "./PresentationLayout.svelte";
import ChatLayout from "./ChatLayout.svelte";
</script>
<div class="video-overlay">
{#if $layoutModeStore === LayoutMode.Presentation }
<PresentationLayout />
{:else }
<ChatLayout />
{/if}
</div>
<style lang="scss">
.video-overlay {
display: flex;
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg2985" version="1.1" inkscape:version="0.48.4 r9939" width="485.33627" height="485.33627" sodipodi:docname="600px-France_road_sign_B1j.svg[1].png">
<metadata id="metadata2991">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs2989"/>
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1272" inkscape:window-height="745" id="namedview2987" showgrid="false" inkscape:snap-global="true" inkscape:snap-grids="true" inkscape:snap-bbox="true" inkscape:bbox-paths="true" inkscape:bbox-nodes="true" inkscape:snap-bbox-edge-midpoints="true" inkscape:snap-bbox-midpoints="true" inkscape:object-paths="true" inkscape:snap-intersection-paths="true" inkscape:object-nodes="true" inkscape:snap-smooth-nodes="true" inkscape:snap-midpoints="true" inkscape:snap-object-midpoints="true" inkscape:snap-center="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:zoom="0.59970176" inkscape:cx="390.56499" inkscape:cy="244.34365" inkscape:window-x="86" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1">
<inkscape:grid type="xygrid" id="grid2995" empspacing="5" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="-57.33186px" originy="-57.33186px"/>
</sodipodi:namedview>
<g inkscape:groupmode="layer" id="layer1" inkscape:label="1" style="display:inline" transform="translate(-57.33186,-57.33186)">
<path sodipodi:type="arc" style="color:#000000;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path2997" sodipodi:cx="300" sodipodi:cy="300" sodipodi:rx="240" sodipodi:ry="240" d="M 540,300 C 540,432.54834 432.54834,540 300,540 167.45166,540 60,432.54834 60,300 60,167.45166 167.45166,60 300,60 432.54834,60 540,167.45166 540,300 z" transform="matrix(1.0058783,0,0,1.0058783,-1.76349,-1.76349)"/>
<path sodipodi:type="arc" style="color:#000000;fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4005" sodipodi:cx="304.75" sodipodi:cy="214.75" sodipodi:rx="44.75" sodipodi:ry="44.75" d="m 349.5,214.75 c 0,24.71474 -20.03526,44.75 -44.75,44.75 -24.71474,0 -44.75,-20.03526 -44.75,-44.75 0,-24.71474 20.03526,-44.75 44.75,-44.75 24.71474,0 44.75,20.03526 44.75,44.75 z" transform="matrix(5.1364411,0,0,5.1364411,-1265.3304,-803.05073)"/>
<rect style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="rect4001" width="345" height="80.599998" x="127.5" y="259.70001"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,27 @@
export function getColorByString(str: string): string | null {
let hash = 0;
if (str.length === 0) {
return null;
}
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 255;
color += ("00" + value.toString(16)).substr(-2);
}
return color;
}
export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
node.srcObject = stream;
return {
update(newStream: MediaStream) {
if (node.srcObject != newStream) {
node.srcObject = newStream;
}
},
};
}

View File

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48.97 39.04"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="0.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M35.08,11.78v8.75H24.31V11.78H35.08m1-1H23.31V21.53H36.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="0.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M34.87,26.05V34.8H24.11V26.05H34.87m1-1H23.11V35.8H35.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="0.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M34.87,40.07v8.75H24.11V40.07H34.87m1-1H23.11V49.82H35.87V39.07Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M53.08,11.78v8.75H42.31V11.78H53.08m1-1H41.31V21.53H54.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M52.87,26.05V34.8H42.11V26.05H52.87m1-1H41.11V35.8H53.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M52.87,40.07v8.75H42.11V40.07H52.87m1-1H41.11V49.82H53.87V39.07Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M71.08,11.78v8.75H60.31V11.78H71.08m1-1H59.31V21.53H72.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M70.87,26.05V34.8H60.11V26.05H70.87m1-1H59.11V35.8H71.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M70.87,40.07v8.75H60.11V40.07H70.87m1-1H59.11V49.82H71.87V39.07Z" transform="translate(-23.11 -10.78)"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.83 54"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="0.5" y="0.5" width="63.13" height="53"/><path class="cls-1" d="M67.12,6V58H5V6H67.12m1-1H4V59H68.12V5Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="0.75" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,6.25V18.12H73.37V6.25H87.83m1-1H72.37V19.12H88.83V5.25Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="17.69" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,23.19V35.05H73.37V23.19H87.83m1-1H72.37V36.05H88.83V22.19Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="34.75" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,40.25V52.12H73.37V40.25H87.83m1-1H72.37V53.12H88.83V39.25Z" transform="translate(-4 -5)"/></svg>

After

Width:  |  Height:  |  Size: 873 B

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 {
BatchMessage,
@ -11,7 +11,8 @@ import {
RoomJoinedMessage,
ServerToClientMessage,
SetPlayerDetailsMessage,
SilentMessage, StopGlobalMessage,
SilentMessage,
StopGlobalMessage,
UserJoinedMessage,
UserLeftMessage,
UserMovedMessage,
@ -31,37 +32,43 @@ import {
EmotePromptMessage,
SendUserMessage,
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 {ProtobufClientUtils} from "../Network/ProtobufClientUtils";
import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
import {
EventMessage,
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface,
MessageUserJoined, OnConnectInterface, PlayGlobalMessageInterface, PositionInterface,
GroupCreatedUpdatedMessageInterface,
ItemEventMessageInterface,
MessageUserJoined,
OnConnectInterface,
PlayGlobalMessageInterface,
PositionInterface,
RoomJoinedMessageInterface,
ViewportInterface, WebRtcDisconnectMessageInterface,
ViewportInterface,
WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels";
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
import {adminMessagesService} from "./AdminMessagesService";
import {worldFullMessageStream} from "./WorldFullMessageStream";
import {worldFullWarningStream} from "./WorldFullWarningStream";
import {connectionManager} from "./ConnectionManager";
import {emoteEventStream} from "./EmoteEventStream";
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import { adminMessagesService } from "./AdminMessagesService";
import { worldFullMessageStream } from "./WorldFullMessageStream";
import { worldFullWarningStream } from "./WorldFullWarningStream";
import { connectionManager } from "./ConnectionManager";
import { emoteEventStream } from "./EmoteEventStream";
const manualPingDelay = 20000;
export class RoomConnection implements RoomConnection {
private readonly socket: WebSocket;
private userId: number|null = null;
private userId: number | null = null;
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 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;
}
@ -70,28 +77,35 @@ export class RoomConnection implements RoomConnection {
* @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]"
*/
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();
url = url.replace('http://', 'ws://').replace('https://', 'wss://');
if (!url.endsWith('/')) {
url += '/';
url = url.replace("http://", "ws://").replace("https://", "wss://");
if (!url.endsWith("/")) {
url += "/";
}
url += 'room';
url += '?roomId='+(roomId ?encodeURIComponent(roomId):'');
url += '&token='+(token ?encodeURIComponent(token):'');
url += '&name='+encodeURIComponent(name);
url += "room";
url += "?roomId=" + (roomId ? encodeURIComponent(roomId) : "");
url += "&token=" + (token ? encodeURIComponent(token) : "");
url += "&name=" + encodeURIComponent(name);
for (const layer of characterLayers) {
url += '&characterLayers='+encodeURIComponent(layer);
url += "&characterLayers=" + encodeURIComponent(layer);
}
url += '&x='+Math.floor(position.x);
url += '&y='+Math.floor(position.y);
url += '&top='+Math.floor(viewport.top);
url += '&bottom='+Math.floor(viewport.bottom);
url += '&left='+Math.floor(viewport.left);
url += '&right='+Math.floor(viewport.right);
if (typeof companion === 'string') {
url += '&companion='+encodeURIComponent(companion);
url += "&x=" + Math.floor(position.x);
url += "&y=" + Math.floor(position.y);
url += "&top=" + Math.floor(viewport.top);
url += "&bottom=" + Math.floor(viewport.bottom);
url += "&left=" + Math.floor(viewport.left);
url += "&right=" + Math.floor(viewport.right);
if (typeof companion === "string") {
url += "&companion=" + encodeURIComponent(companion);
}
if (RoomConnection.websocketFactory) {
@ -100,9 +114,9 @@ export class RoomConnection implements RoomConnection {
this.socket = new WebSocket(url);
}
this.socket.binaryType = 'arraybuffer';
this.socket.binaryType = "arraybuffer";
let interval: ReturnType<typeof setInterval>|undefined = undefined;
let interval: ReturnType<typeof setInterval> | undefined = undefined;
this.socket.onopen = (ev) => {
//we manually ping every 20s to not be logged out by the server, even when the game is in background.
@ -110,7 +124,7 @@ export class RoomConnection implements RoomConnection {
interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
};
this.socket.addEventListener('close', (event) => {
this.socket.addEventListener("close", (event) => {
if (interval) {
clearInterval(interval);
}
@ -127,7 +141,7 @@ export class RoomConnection implements RoomConnection {
if (message.hasBatchmessage()) {
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
let event: string|null = null;
let event: string | null = null;
let payload;
if (subMessage.hasUsermovedmessage()) {
event = EventMessage.USER_MOVED;
@ -151,7 +165,7 @@ export class RoomConnection implements RoomConnection {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else {
throw new Error('Unexpected batch message type');
throw new Error("Unexpected batch message type");
}
if (event) {
@ -161,7 +175,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasRoomjoinedmessage()) {
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
const items: { [itemId: number] : unknown } = {};
const items: { [itemId: number]: unknown } = {};
for (const item of roomJoinedMessage.getItemList()) {
items[item.getItemid()] = JSON.parse(item.getStatejson());
}
@ -172,8 +186,8 @@ export class RoomConnection implements RoomConnection {
this.dispatch(EventMessage.CONNECT, {
connection: this,
room: {
items
} as RoomJoinedMessageInterface
items,
} as RoomJoinedMessageInterface,
});
} else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage();
@ -181,10 +195,13 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
this.closed = true;
}else if (message.hasWebrtcsignaltoclientmessage()) {
} else if (message.hasWebrtcsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage());
this.dispatch(
EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL,
message.getWebrtcscreensharingsignaltoclientmessage()
);
} else if (message.hasWebrtcstartmessage()) {
this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
} else if (message.hasWebrtcdisconnectmessage()) {
@ -206,10 +223,9 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else {
throw new Error('Unknown message received');
}
throw new Error("Unknown message received");
}
};
}
private dispatch(event: string, payload: unknown): void {
@ -238,22 +254,22 @@ export class RoomConnection implements RoomConnection {
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();
positionMessage.setX(Math.floor(x));
positionMessage.setY(Math.floor(y));
let directionEnum: Direction;
switch (direction) {
case 'up':
case "up":
directionEnum = Direction.UP;
break;
case 'down':
case "down":
directionEnum = Direction.DOWN;
break;
case 'left':
case "left":
directionEnum = Direction.LEFT;
break;
case 'right':
case "right":
directionEnum = Direction.RIGHT;
break;
default:
@ -275,8 +291,8 @@ export class RoomConnection implements RoomConnection {
return viewportMessage;
}
public sharePosition(x : number, y : number, direction : string, moving: boolean, viewport: ViewportInterface) : void{
if(!this.socket){
public sharePosition(x: number, y: number, direction: string, moving: boolean, viewport: ViewportInterface): void {
if (!this.socket) {
return;
}
@ -328,15 +344,17 @@ export class RoomConnection implements RoomConnection {
private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
const position = message.getPosition();
if (position === undefined) {
throw new Error('Invalid JOIN_ROOM message');
throw new Error("Invalid JOIN_ROOM message");
}
const characterLayers = message.getCharacterlayersList().map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
const characterLayers = message
.getCharacterlayersList()
.map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
return {
name: characterLayer.getName(),
img: characterLayer.getUrl()
}
})
img: characterLayer.getUrl(),
};
});
const companion = message.getCompanion();
@ -346,8 +364,8 @@ export class RoomConnection implements RoomConnection {
characterLayers,
visitCardUrl: message.getVisitcardurl(),
position: ProtobufClientUtils.toPointInterface(position),
companion: companion ? companion.getName() : null
}
companion: companion ? companion.getName() : null,
};
}
public onUserMoved(callback: (message: UserMovedMessage) => void): void {
@ -373,7 +391,9 @@ export class RoomConnection implements RoomConnection {
});
}
public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void {
public onGroupUpdatedOrCreated(
callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void
): void {
this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => {
callback(this.toGroupCreatedUpdatedMessage(message));
});
@ -382,14 +402,14 @@ export class RoomConnection implements RoomConnection {
private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
const position = message.getPosition();
if (position === undefined) {
throw new Error('Missing position in GROUP_CREATE_UPDATE');
throw new Error("Missing position in GROUP_CREATE_UPDATE");
}
return {
groupId: message.getGroupid(),
position: position.toObject(),
groupSize: message.getGroupsize()
}
groupSize: message.getGroupsize(),
};
}
public onGroupDeleted(callback: (groupId: number) => void): void {
@ -405,7 +425,7 @@ export class RoomConnection implements RoomConnection {
}
public onConnectError(callback: (error: Event) => void): void {
this.socket.addEventListener('error', callback)
this.socket.addEventListener("error", callback);
}
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
@ -477,11 +497,11 @@ export class RoomConnection implements RoomConnection {
}
public onServerDisconnected(callback: () => void): void {
this.socket.addEventListener('close', (event) => {
this.socket.addEventListener("close", (event) => {
if (this.closed === true || connectionManager.unloading) {
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) {
// Normal closure case
return;
@ -491,14 +511,14 @@ export class RoomConnection implements RoomConnection {
}
public getUserId(): number {
if (this.userId === null) throw 'UserId cannot be null!'
if (this.userId === null) throw "UserId cannot be null!";
return this.userId;
}
disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
callback({
userId: message.getUserid()
userId: message.getUserid(),
});
});
}
@ -522,21 +542,22 @@ export class RoomConnection implements RoomConnection {
itemId: message.getItemid(),
event: message.getEvent(),
parameters: JSON.parse(message.getParametersjson()),
state: JSON.parse(message.getStatejson())
state: JSON.parse(message.getStatejson()),
});
});
}
public uploadAudio(file : FormData){
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file).then((res: {data:{}}) => {
public uploadAudio(file: FormData) {
return Axios.post(`${UPLOADER_URL}/upload-audio-message`, file)
.then((res: { data: {} }) => {
return res.data;
}).catch((err) => {
})
.catch((err) => {
console.error(err);
throw err;
});
}
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
callback({
@ -559,7 +580,7 @@ export class RoomConnection implements RoomConnection {
});
}
public emitGlobalMessage(message: PlayGlobalMessageInterface){
public emitGlobalMessage(message: PlayGlobalMessageInterface) {
const playGlobalMessage = new PlayGlobalMessage();
playGlobalMessage.setId(message.id);
playGlobalMessage.setType(message.type);
@ -571,7 +592,7 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public emitReportPlayerMessage(reportedUserId: number, reportComment: string ): void {
public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void {
const reportPlayerMessage = new ReportPlayerMessage();
reportPlayerMessage.setReporteduserid(reportedUserId);
reportPlayerMessage.setReportcomment(reportComment);
@ -582,7 +603,7 @@ export class RoomConnection implements RoomConnection {
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();
queryJitsiJwtMessage.setJitsiroom(jitsiRoom);
if (tag !== undefined) {
@ -606,16 +627,20 @@ export class RoomConnection implements RoomConnection {
}
public isAdmin(): boolean {
return this.hasTag('admin');
return this.hasTag("admin");
}
public emitEmoteEvent(emoteName: string): void {
const emoteMessage = new EmotePromptMessage();
emoteMessage.setEmote(emoteName)
emoteMessage.setEmote(emoteName);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setEmotepromptmessage(emoteMessage);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public getAllTags(): string[] {
return this.tags;
}
}

View File

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

View File

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

View File

@ -1,7 +1,13 @@
import type { ITiledMap, ITiledMapLayerProperty } from "../Map/ITiledMap";
import { LayersIterator } from "../Map/LayersIterator";
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
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;
/**
* A wrapper around a ITiledMap interface to provide additional capabilities.
@ -12,41 +18,52 @@ export class GameMap {
private lastProperties = new Map<string, string | boolean | number>();
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {}
public readonly layersIterator: LayersIterator;
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {};
public readonly flatLayers: ITiledMapLayer[];
public readonly phaserLayers: TilemapLayer[] = [];
public exitUrls: Array<string> = []
public exitUrls: Array<string> = [];
public hasStartTile = false;
public constructor(private map: ITiledMap) {
this.layersIterator = new LayersIterator(map);
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 => {
tileset?.tiles?.forEach((tile) => {
if (tile.properties) {
this.tileSetPropertyMap[tileset.firstgid + tile.id] = tile.properties
tile.properties.forEach(prop => {
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);
} else if (prop.name == "start") {
this.hasStartTile = true
this.hasStartTile = true;
}
})
});
}
})
});
}
}
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index]
return this.tileSetPropertyMap[index];
}
return []
return [];
}
/**
* Sets the position of the current player (in pixels)
* This will trigger events if properties are changing.
@ -88,8 +105,8 @@ export class GameMap {
private getProperties(key: number): Map<string, string | boolean | number> {
const properties = new Map<string, string | boolean | number>();
for (const layer of this.layersIterator) {
if (layer.type !== 'tilelayer') {
for (const layer of this.flatLayers) {
if (layer.type !== "tilelayer") {
continue;
}
@ -99,7 +116,7 @@ export class GameMap {
if (tiles[key] == 0) {
continue;
}
tileIndex = tiles[key]
tileIndex = tiles[key];
}
// There is a tile in this layer, let's embed the properties
@ -113,20 +130,29 @@ export class GameMap {
}
if (tileIndex) {
this.tileSetPropertyMap[tileIndex]?.forEach(property => {
this.tileSetPropertyMap[tileIndex]?.forEach((property) => {
if (property.value) {
properties.set(property.name, property.value)
properties.set(property.name, property.value);
} else if (properties.has(property.name)) {
properties.delete(property.name)
properties.delete(property.name);
}
})
});
}
}
return properties;
}
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) {
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>
) {
const callbacksArray = this.callbacks.get(propName);
if (callbacksArray !== undefined) {
for (const callback of callbacksArray) {
@ -146,4 +172,18 @@ export class GameMap {
}
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,10 +1,14 @@
import type {HasMovedEvent} from "./GameManager";
import {MAX_EXTRAPOLATION_TIME} from "../../Enum/EnvironmentVariable";
import type {PositionInterface} from "../../Connexion/ConnexionModels";
import { MAX_EXTRAPOLATION_TIME } from "../../Enum/EnvironmentVariable";
import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
export class PlayerMovement {
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) {
}
public constructor(
private startPosition: PositionInterface,
private startTick: number,
private endPosition: HasPlayerMovedEvent,
private endTick: number
) {}
public isOutdated(tick: number): boolean {
//console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME)
@ -17,21 +21,25 @@ export class PlayerMovement {
return tick > this.endTick + MAX_EXTRAPOLATION_TIME;
}
public getPosition(tick: number): HasMovedEvent {
public getPosition(tick: number): HasPlayerMovedEvent {
// Special case: end position reached and end position is not moving
if (tick >= this.endTick && this.endPosition.moving === false) {
//console.log('Movement finished ', this.endPosition)
return this.endPosition;
}
const x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x;
const y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y;
const x =
(this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.x;
const y =
(this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.y;
//console.log('Computed position ', x, y)
return {
x,
y,
direction: this.endPosition.direction,
moving: true
}
moving: true,
};
}
}

View File

@ -2,13 +2,13 @@
* This class is in charge of computing the position of all players.
* Player movement is delayed by 200ms so position depends on ticks.
*/
import type {PlayerMovement} from "./PlayerMovement";
import type {HasMovedEvent} from "./GameManager";
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import type { PlayerMovement } from "./PlayerMovement";
export class PlayersPositionInterpolator {
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);
}
@ -16,15 +16,15 @@ export class PlayersPositionInterpolator {
this.playerMovements.delete(userId);
}
getUpdatedPositions(tick: number) : Map<number, HasMovedEvent> {
const positions = new Map<number, HasMovedEvent>();
getUpdatedPositions(tick: number): Map<number, HasPlayerMovedEvent> {
const positions = new Map<number, HasPlayerMovedEvent>();
this.playerMovements.forEach((playerMovement: PlayerMovement, userId: number) => {
if (playerMovement.isOutdated(tick)) {
//console.log("outdated")
this.playerMovements.delete(userId);
}
//console.log("moving")
positions.set(userId, playerMovement.getPosition(tick))
positions.set(userId, playerMovement.getPosition(tick));
});
return positions;
}

View File

@ -1,19 +1,18 @@
import type { PositionInterface } from '../../Connexion/ConnexionModels';
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from '../Map/ITiledMap';
import type { GameMap } from './GameMap';
import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
import type { GameMap } from "./GameMap";
const defaultStartLayerName = 'start';
const defaultStartLayerName = "start";
export class StartPositionCalculator {
public startPosition!: PositionInterface
public startPosition!: PositionInterface;
constructor(
private readonly gameMap: GameMap,
private readonly mapFile: ITiledMap,
private readonly initPosition: PositionInterface | null,
private readonly startLayerName: string | null) {
public readonly startLayerName: string | null
) {
this.initStartXAndStartY();
}
private initStartXAndStartY() {
@ -32,11 +31,13 @@ export class StartPositionCalculator {
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn('This map is missing a layer named "start" that contains the available default start positions.');
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16
y: this.mapFile.height * 16,
};
}
}
@ -48,20 +49,23 @@ export class StartPositionCalculator {
*/
public initPositionFromLayerName(selectedOrDefaultLayer: string | null, selectedLayer: string | null) {
if (!selectedOrDefaultLayer) {
selectedOrDefaultLayer = defaultStartLayerName
selectedOrDefaultLayer = defaultStartLayerName;
}
for (const layer of this.gameMap.layersIterator) {
if ((selectedOrDefaultLayer === layer.name || layer.name.endsWith('/' + selectedOrDefaultLayer)) && layer.type === 'tilelayer' && (selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))) {
if (
(selectedOrDefaultLayer === layer.name || layer.name.endsWith("/" + selectedOrDefaultLayer)) &&
layer.type === "tilelayer" &&
(selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))
) {
const startPosition = this.startUser(layer, selectedLayer);
this.startPosition = {
x: startPosition.x + this.mapFile.tilewidth / 2,
y: startPosition.y + this.mapFile.tileheight / 2
y: startPosition.y + this.mapFile.tileheight / 2,
};
}
}
}
}
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, "startLayer") == true;
}
@ -73,8 +77,8 @@ export class StartPositionCalculator {
*/
private startUser(selectedOrDefaultLayer: ITiledMapTileLayer, selectedLayer: string | null): PositionInterface {
const tiles = selectedOrDefaultLayer.data;
if (typeof (tiles) === 'string') {
throw new Error('The content of a JSON map must be filled as a JSON array, not as a string');
if (typeof tiles === "string") {
throw new Error("The content of a JSON map must be filled as a JSON array, not as a string");
}
const possibleStartPositions: PositionInterface[] = [];
tiles.forEach((objectKey: number, key: number) => {
@ -86,8 +90,11 @@ export class StartPositionCalculator {
if (selectedLayer && this.gameMap.hasStartTile) {
const properties = this.gameMap.getPropertiesForIndex(objectKey);
if (!properties.length || !properties.some(property => property.name == "start" && property.value == selectedLayer)) {
return
if (
!properties.length ||
!properties.some((property) => property.name == "start" && property.value == selectedLayer)
) {
return;
}
}
possibleStartPositions.push({ x: x * this.mapFile.tilewidth, y: y * this.mapFile.tilewidth });
@ -97,7 +104,7 @@ export class StartPositionCalculator {
console.warn('The start layer "' + selectedOrDefaultLayer.name + '" for this map is empty.');
return {
x: 0,
y: 0
y: 0,
};
}
// Choose one of the available start positions at random amongst the list of available start positions.
@ -109,7 +116,9 @@ export class StartPositionCalculator {
if (!properties) {
return undefined;
}
const obj = properties.find((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase());
const obj = properties.find(
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
);
if (obj === undefined) {
return undefined;
}

View File

@ -4,6 +4,8 @@
* Represents the interface for the Tiled exported data structure (JSON). Used
* when loading resources via Resource loader.
*/
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
export interface ITiledMap {
width: number;
height: number;
@ -34,7 +36,7 @@ export interface ITiledMap {
export interface ITiledMapLayerProperty {
name: string;
type: string;
value: string|boolean|number|undefined;
value: string | boolean | number | undefined;
}
/*export interface ITiledMapLayerBooleanProperty {
@ -46,7 +48,7 @@ export interface ITiledMapLayerProperty {
export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
export interface ITiledMapGroupLayer {
id?: number,
id?: number;
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
@ -62,8 +64,8 @@ export interface ITiledMapGroupLayer {
}
export interface ITiledMapTileLayer {
id?: number,
data: number[]|string;
id?: number;
data: number[] | string;
height: number;
name: string;
opacity: number;
@ -81,10 +83,11 @@ export interface ITiledMapTileLayer {
* Draw order (topdown (default), index)
*/
draworder?: string;
phaserLayer?: TilemapLayer;
}
export interface ITiledMapObjectLayer {
id?: number,
id?: number;
height: number;
name: string;
opacity: number;
@ -114,7 +117,7 @@ export interface ITiledMapObject {
gid: number;
height: number;
name: string;
properties: {[key: string]: string};
properties: { [key: string]: string };
rotation: number;
type: string;
visible: boolean;
@ -130,26 +133,26 @@ export interface ITiledMapObject {
/**
* Polygon points
*/
polygon: {x: number, y: number}[];
polygon: { x: number; y: number }[];
/**
* Polyline points
*/
polyline: {x: number, y: number}[];
polyline: { x: number; y: number }[];
text?: ITiledText
text?: ITiledText;
}
export interface ITiledText {
text: string,
wrap?: boolean,
fontfamily?: string,
pixelsize?: number,
color?: string,
underline?: boolean,
italic?: boolean,
strikeout?: boolean,
halign?: "center"|"right"|"justify"|"left"
text: string;
wrap?: boolean;
fontfamily?: string;
pixelsize?: number;
color?: string;
underline?: boolean;
italic?: boolean;
strikeout?: boolean;
halign?: "center" | "right" | "justify" | "left";
}
export interface ITiledTileSet {
@ -160,7 +163,7 @@ export interface ITiledTileSet {
imagewidth: number;
margin: number;
name: string;
properties: {[key: string]: string};
properties: { [key: string]: string };
spacing: number;
tilecount: number;
tileheight: number;
@ -176,10 +179,10 @@ export interface ITiledTileSet {
}
export interface ITile {
id: number,
type?: string
id: number;
type?: string;
properties?: Array<ITiledMapLayerProperty>
properties?: Array<ITiledMapLayerProperty>;
}
export interface ITiledMapTerrain {

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

@ -1,24 +1,29 @@
import {LoginScene, LoginSceneName} from "../Login/LoginScene";
import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene";
import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene";
import {gameManager} from "../Game/GameManager";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {mediaManager} from "../../WebRtc/MediaManager";
import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore";
import {videoConstraintStore} from "../../Stores/MediaStore";
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
import {get} from "svelte/store";
import { LoginScene, LoginSceneName } from "../Login/LoginScene";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene";
import { gameManager } from "../Game/GameManager";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../../Url/UrlManager";
import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer";
import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream";
import { menuIconVisible } from "../../Stores/MenuStore";
import { videoConstraintStore } from "../../Stores/MediaStore";
import { 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';
const gameMenuKey = 'gameMenu';
const gameMenuIconKey = 'gameMenuIcon';
const gameSettingsMenuKey = 'gameSettingsMenu';
const gameShare = 'gameShare';
export const MenuSceneName = "MenuScene";
const gameMenuKey = "gameMenu";
const gameMenuIconKey = "gameMenuIcon";
const gameSettingsMenuKey = "gameSettingsMenu";
const gameShare = "gameShare";
const closedSideMenuX = -1000;
const openedSideMenuX = 0;
@ -39,19 +44,49 @@ export class MenuScene extends Phaser.Scene {
private menuButton!: Phaser.GameObjects.DOMElement;
private warningContainer: WarningContainer | null = null;
private warningContainerTimeout: NodeJS.Timeout | null = null;
private subscriptions = new Subscription();
constructor() {
super({key: MenuSceneName});
super({ key: MenuSceneName });
this.gameQualityValue = localUserStore.getGameQualityValue();
this.videoQualityValue = localUserStore.getVideoQualityValue();
this.subscriptions.add(
registerMenuCommandStream.subscribe((menuCommand) => {
this.addMenuOption(menuCommand);
})
);
this.subscriptions.add(
iframeListener.unregisterMenuCommandStream.subscribe((menuCommand) => {
this.destroyMenu(menuCommand);
})
);
}
preload () {
this.load.html(gameMenuKey, 'resources/html/gameMenu.html');
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html');
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html');
this.load.html(gameShare, 'resources/html/gameShare.html');
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(gameMenuIconKey, "resources/html/gameMenuIcon.html");
this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
this.load.html(gameShare, "resources/html/gameShare.html");
this.load.html(gameReportKey, gameReportRessource);
this.load.html(warningContainerKey, warningContainerHtml);
}
@ -60,42 +95,46 @@ export class MenuScene extends Phaser.Scene {
menuIconVisible.set(true);
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
this.menuElement.setOrigin(0);
MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu');
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
const middleX = (window.innerWidth / 3) - 298;
const middleX = window.innerWidth / 3 - 298;
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality');
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
this.gameShareElement.addListener('click');
this.gameShareElement.on('click', (event:MouseEvent) => {
this.gameShareElement.addListener("click");
this.gameShareElement.on("click", (event: MouseEvent) => {
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'gameShareFormSubmit') {
if ((event?.target as HTMLInputElement).id === "gameShareFormSubmit") {
this.copyLink();
}else if((event?.target as HTMLInputElement).id === 'gameShareFormCancel') {
} else if ((event?.target as HTMLInputElement).id === "gameShareFormCancel") {
this.closeGameShare();
}
});
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
mediaManager.setShowReportModalCallBacks((userId, userName) => {
this.gameReportElement = new ReportMenu(
this,
connectionManager.getConnexionType === GameConnexionTypes.anonymous
);
showReportScreenStore.subscribe((user) => {
if (user !== null) {
this.closeAll();
this.gameReportElement.open(parseInt(userId), userName);
this.gameReportElement.open(user.userId, user.userName);
}
});
this.input.keyboard.on('keyup-TAB', () => {
this.input.keyboard.on("keyup-TAB", () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
});
this.menuButton = this.add.dom(0, 0).createFromCache(gameMenuIconKey);
this.menuButton.addListener('click');
this.menuButton.on('click', () => {
this.menuButton.addListener("click");
this.menuButton.on("click", () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
});
this.menuElement.addListener('click');
this.menuElement.on('click', this.onMenuClick.bind(this));
this.menuElement.addListener("click");
this.menuElement.on("click", this.onMenuClick.bind(this));
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
}
@ -112,7 +151,7 @@ export class MenuScene extends Phaser.Scene {
public revealMenuIcon(): void {
//TODO fix me: add try catch because at the same time, 'this.menuButton' variable doesn't exist and there is error on 'getChildByID' function
try {
(this.menuButton.getChildByID('menuIcon') as HTMLElement).hidden = false;
(this.menuButton.getChildByID("menuIcon") as HTMLElement).hidden = false;
} catch (err) {
console.error(err);
}
@ -122,22 +161,22 @@ export class MenuScene extends Phaser.Scene {
if (this.sideMenuOpened) return;
this.closeAll();
this.sideMenuOpened = true;
this.menuButton.getChildByID('openMenuButton').innerHTML = 'X';
this.menuButton.getChildByID("openMenuButton").innerHTML = "X";
const connection = gameManager.getCurrentGameScene(this).connection;
if (connection && connection.isAdmin()) {
const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement;
const adminSection = this.menuElement.getChildByID("adminConsoleSection") as HTMLElement;
adminSection.hidden = false;
}
//TODO bind with future metadata of card
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement;
const adminSection = this.menuElement.getChildByID("socialLinks") as HTMLElement;
adminSection.hidden = false;
//}
this.tweens.add({
targets: this.menuElement,
x: openedSideMenuX,
duration: 500,
ease: 'Power3'
ease: "Power3",
});
}
@ -150,23 +189,22 @@ export class MenuScene extends Phaser.Scene {
}
this.warningContainerTimeout = setTimeout(() => {
this.warningContainer?.destroy();
this.warningContainer = null
this.warningContainerTimeout = null
this.warningContainer = null;
this.warningContainerTimeout = null;
}, 120000);
}
private closeSideMenu(): void {
if (!this.sideMenuOpened) return;
this.sideMenuOpened = false;
this.closeAll();
this.menuButton.getChildByID('openMenuButton').innerHTML = `<img src="/static/images/menu.svg">`;
this.menuButton.getChildByID("openMenuButton").innerHTML = `<img src="/static/images/menu.svg">`;
consoleGlobalMessageManagerVisibleStore.set(false);
this.tweens.add({
targets: this.menuElement,
x: closedSideMenuX,
duration: 500,
ease: 'Power3'
ease: "Power3",
});
}
@ -180,29 +218,33 @@ export class MenuScene extends Phaser.Scene {
this.settingsMenuOpened = true;
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement;
gameQualitySelect.value = ''+this.gameQualityValue;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement;
videoQualitySelect.value = ''+this.videoQualityValue;
const gameQualitySelect = this.gameQualityMenuElement.getChildByID("select-game-quality") as HTMLInputElement;
gameQualitySelect.value = "" + this.gameQualityValue;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID("select-video-quality") as HTMLInputElement;
videoQualitySelect.value = "" + this.videoQualityValue;
this.gameQualityMenuElement.addListener('click');
this.gameQualityMenuElement.on('click', (event:MouseEvent) => {
this.gameQualityMenuElement.addListener("click");
this.gameQualityMenuElement.on("click", (event: MouseEvent) => {
event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') {
const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement;
if ((event?.target as HTMLInputElement).id === "gameQualityFormSubmit") {
const gameQualitySelect = this.gameQualityMenuElement.getChildByID(
"select-game-quality"
) as HTMLInputElement;
const videoQualitySelect = this.gameQualityMenuElement.getChildByID(
"select-video-quality"
) as HTMLInputElement;
this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value));
} else if((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') {
} else if ((event?.target as HTMLInputElement).id === "gameQualityFormCancel") {
this.closeGameQualityMenu();
}
});
let middleY = this.scale.height / 2 - 392/2;
if(middleY < 0){
let middleY = this.scale.height / 2 - 392 / 2;
if (middleY < 0) {
middleY = 0;
}
let middleX = this.scale.width / 2 - 457/2;
if(middleX < 0){
let middleX = this.scale.width / 2 - 457 / 2;
if (middleX < 0) {
middleX = 0;
}
this.tweens.add({
@ -210,7 +252,7 @@ export class MenuScene extends Phaser.Scene {
y: middleY,
x: middleX,
duration: 1000,
ease: 'Power3'
ease: "Power3",
});
}
@ -218,17 +260,16 @@ export class MenuScene extends Phaser.Scene {
if (!this.settingsMenuOpened) return;
this.settingsMenuOpened = false;
this.gameQualityMenuElement.removeListener('click');
this.gameQualityMenuElement.removeListener("click");
this.tweens.add({
targets: this.gameQualityMenuElement,
y: -400,
duration: 1000,
ease: 'Power3'
ease: "Power3",
});
}
private openGameShare(): void{
private openGameShare(): void {
if (this.gameShareOpened) {
this.closeGameShare();
return;
@ -236,17 +277,17 @@ export class MenuScene extends Phaser.Scene {
//close all
this.closeAll();
const gameShareLink = this.gameShareElement.getChildByID('gameShareLink') as HTMLInputElement;
const gameShareLink = this.gameShareElement.getChildByID("gameShareLink") as HTMLInputElement;
gameShareLink.value = location.toString();
this.gameShareOpened = true;
let middleY = this.scale.height / 2 - 85;
if(middleY < 0){
if (middleY < 0) {
middleY = 0;
}
let middleX = this.scale.width / 2 - 200;
if(middleX < 0){
if (middleX < 0) {
middleX = 0;
}
this.tweens.add({
@ -254,58 +295,64 @@ export class MenuScene extends Phaser.Scene {
y: middleY,
x: middleX,
duration: 1000,
ease: 'Power3'
ease: "Power3",
});
}
private closeGameShare(): void{
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement;
gameShareInfo.innerText = '';
gameShareInfo.style.display = 'none';
private closeGameShare(): void {
const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
gameShareInfo.innerText = "";
gameShareInfo.style.display = "none";
this.gameShareOpened = false;
this.tweens.add({
targets: this.gameShareElement,
y: -400,
duration: 1000,
ease: 'Power3'
ease: "Power3",
});
}
private onMenuClick(event:MouseEvent) {
if((event?.target as HTMLInputElement).classList.contains('not-button')){
private onMenuClick(event: MouseEvent) {
const htmlMenuItem = event?.target as HTMLInputElement;
if (htmlMenuItem.classList.contains("not-button")) {
return;
}
event.preventDefault();
if (htmlMenuItem.classList.contains("fromApi")) {
sendMenuClickedEvent(htmlMenuItem.id);
return;
}
switch ((event?.target as HTMLInputElement).id) {
case 'changeNameButton':
case "changeNameButton":
this.closeSideMenu();
gameManager.leaveGame(this, LoginSceneName, new LoginScene());
break;
case 'sparkButton':
case "sparkButton":
this.gotToCreateMapPage();
break;
case 'changeSkinButton':
case "changeSkinButton":
this.closeSideMenu();
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
break;
case 'changeCompanionButton':
case "changeCompanionButton":
this.closeSideMenu();
gameManager.leaveGame(this, SelectCompanionSceneName, new SelectCompanionScene());
break;
case 'closeButton':
case "closeButton":
this.closeSideMenu();
break;
case 'shareButton':
case "shareButton":
this.openGameShare();
break;
case 'editGameSettingsButton':
case "editGameSettingsButton":
this.openGameSettingsMenu();
break;
case 'toggleFullscreen':
case "toggleFullscreen":
this.toggleFullscreen();
break;
case 'adminConsoleButton':
case "adminConsoleButton":
if (get(consoleGlobalMessageManagerVisibleStore)) {
consoleGlobalMessageManagerVisibleStore.set(false);
} else {
@ -317,12 +364,12 @@ export class MenuScene extends Phaser.Scene {
private async copyLink() {
await navigator.clipboard.writeText(location.toString());
const gameShareInfo = this.gameShareElement.getChildByID('gameShareInfo') as HTMLParagraphElement;
gameShareInfo.innerText = 'Link copied, you can share it now!';
gameShareInfo.style.display = 'block';
const gameShareInfo = this.gameShareElement.getChildByID("gameShareInfo") as HTMLParagraphElement;
gameShareInfo.innerText = "Link copied, you can share it now!";
gameShareInfo.style.display = "block";
}
private saveSetting(valueGame: number, valueVideo: number){
private saveSetting(valueGame: number, valueVideo: number) {
if (valueGame !== this.gameQualityValue) {
this.gameQualityValue = valueGame;
localUserStore.setGameQualityValue(valueGame);
@ -339,27 +386,31 @@ export class MenuScene extends Phaser.Scene {
private gotToCreateMapPage() {
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
//TODO fix me: this button can to send us on WorkAdventure BO.
const sparkHost = 'https://workadventu.re/getting-started';
window.open(sparkHost, '_blank');
const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank");
}
private closeAll(){
private closeAll() {
this.closeGameQualityMenu();
this.closeGameShare();
this.gameReportElement.close();
}
private toggleFullscreen() {
const body = document.querySelector('body')
const body = document.querySelector("body");
if (body) {
if (document.fullscreenElement ?? document.fullscreen) {
document.exitFullscreen()
document.exitFullscreen();
} else {
body.requestFullscreen();
}
}
}
public destroyMenu(menu: string) {
this.menuElement.getChildByID(menu).remove();
}
public isDirty(): boolean {
return false;
}

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