Merge branch 'develop' into user_interface
# Conflicts: # front/dist/resources/style/style.css
|
@ -1 +1,7 @@
|
||||||
DEBUG_MODE=false
|
DEBUG_MODE=false
|
||||||
|
JITSI_URL=meet.jit.si
|
||||||
|
# If your Jitsi environment has authentication set up, you MUST set JITSI_PRIVATE_MODE to "true" and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret
|
||||||
|
JITSI_PRIVATE_MODE=false
|
||||||
|
JITSI_ISS=
|
||||||
|
SECRET_JITSI_KEY=
|
||||||
|
ADMIN_API_TOKEN=123
|
||||||
|
|
38
.github/workflows/build-and-deploy.yml
vendored
|
@ -20,13 +20,13 @@ jobs:
|
||||||
|
|
||||||
|
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@master
|
- uses: rlespinasse/github-slug-action@1.1.1
|
||||||
|
|
||||||
- name: "Build and push front image"
|
- name: "Build and push front image"
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
dockerfile: front/Dockerfile
|
dockerfile: front/Dockerfile
|
||||||
path: front/
|
path: ./
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-front
|
repository: thecodingmachine/workadventure-front
|
||||||
|
@ -43,13 +43,13 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@master
|
- uses: rlespinasse/github-slug-action@1.1.1
|
||||||
|
|
||||||
- name: "Build and push back image"
|
- name: "Build and push back image"
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
dockerfile: back/Dockerfile
|
dockerfile: back/Dockerfile
|
||||||
path: back/
|
path: ./
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-back
|
repository: thecodingmachine/workadventure-back
|
||||||
|
@ -66,7 +66,7 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@master
|
- uses: rlespinasse/github-slug-action@1.1.1
|
||||||
|
|
||||||
- name: "Build and push back image"
|
- name: "Build and push back image"
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
|
@ -79,6 +79,30 @@ jobs:
|
||||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
|
build-maps:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
|
||||||
|
# Create a slugified value of the branch
|
||||||
|
- uses: rlespinasse/github-slug-action@1.1.1
|
||||||
|
|
||||||
|
- name: "Build and push front image"
|
||||||
|
uses: docker/build-push-action@v1
|
||||||
|
with:
|
||||||
|
dockerfile: maps/Dockerfile
|
||||||
|
path: maps/
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
repository: thecodingmachine/workadventure-maps
|
||||||
|
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||||
|
add_git_labels: true
|
||||||
|
|
||||||
deeploy:
|
deeploy:
|
||||||
needs:
|
needs:
|
||||||
- build-front
|
- build-front
|
||||||
|
@ -96,6 +120,10 @@ jobs:
|
||||||
uses: thecodingmachine/deeployer@master
|
uses: thecodingmachine/deeployer@master
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
||||||
|
ADMIN_API_TOKEN: ${{ secrets.ADMIN_API_TOKEN }}
|
||||||
|
JITSI_ISS: ${{ secrets.JITSI_ISS }}
|
||||||
|
JITSI_URL: ${{ secrets.JITSI_URL }}
|
||||||
|
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
||||||
with:
|
with:
|
||||||
namespace: workadventure-${{ env.GITHUB_REF_SLUG }}
|
namespace: workadventure-${{ env.GITHUB_REF_SLUG }}
|
||||||
|
|
||||||
|
|
30
.github/workflows/continuous_integration.yml
vendored
|
@ -20,16 +20,29 @@ jobs:
|
||||||
- name: "Setup NodeJS"
|
- name: "Setup NodeJS"
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '14.x'
|
||||||
|
|
||||||
|
- name: Install Protoc
|
||||||
|
uses: arduino/setup-protoc@v1
|
||||||
|
with:
|
||||||
|
version: '3.x'
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: yarn install
|
run: yarn install
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
|
- name: "Install messages dependencies"
|
||||||
|
run: yarn install
|
||||||
|
working-directory: "messages"
|
||||||
|
|
||||||
|
- name: "Build proto messages"
|
||||||
|
run: yarn run proto && yarn run copy-to-front
|
||||||
|
working-directory: "messages"
|
||||||
|
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: yarn run build
|
run: yarn run build
|
||||||
env:
|
env:
|
||||||
API_URL: "http://localhost:8080"
|
API_URL: "localhost:8080"
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
- name: "Lint"
|
- name: "Lint"
|
||||||
|
@ -54,10 +67,23 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '12.x'
|
||||||
|
|
||||||
|
- name: Install Protoc
|
||||||
|
uses: arduino/setup-protoc@v1
|
||||||
|
with:
|
||||||
|
version: '3.x'
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: yarn install
|
run: yarn install
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
|
||||||
|
- name: "Install messages dependencies"
|
||||||
|
run: yarn install
|
||||||
|
working-directory: "messages"
|
||||||
|
|
||||||
|
- name: "Build proto messages"
|
||||||
|
run: yarn run proto && yarn run copy-to-back
|
||||||
|
working-directory: "messages"
|
||||||
|
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: yarn run tsc
|
run: yarn run tsc
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
|
BIN
README-INTRO.jpg
Normal file
After Width: | Height: | Size: 386 KiB |
52
README.md
|
@ -1,5 +1,9 @@
|
||||||
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg)
|
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg)
|
||||||
|
|
||||||
|
![WorkAdventure landscape image](README-INTRO.jpg)
|
||||||
|
|
||||||
|
Demo here : [https://workadventu.re/](https://workadventu.re/).
|
||||||
|
|
||||||
# Work Adventure
|
# Work Adventure
|
||||||
|
|
||||||
## Work in progress
|
## Work in progress
|
||||||
|
@ -32,54 +36,6 @@ Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
|
||||||
workadventure.localhost 127.0.0.1
|
workadventure.localhost 127.0.0.1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Designing a map
|
|
||||||
|
|
||||||
If you want to design your own map, you can use [Tiled](https://www.mapeditor.org/).
|
|
||||||
|
|
||||||
A few things to notice:
|
|
||||||
|
|
||||||
- your map can have as many layers as you want
|
|
||||||
- your map MUST contain a layer named "floorLayer" of type "objectgroup" that represents the layer on which characters will be drawn.
|
|
||||||
- the tilesets in your map MUST be embedded. You cannot refer to an external typeset in a TSX file. Click the "embed tileset" button in the tileset tab to embed tileset data.
|
|
||||||
- your map MUST be exported in JSON format. You need to use a recent version of Tiled to get JSON format export (1.3+)
|
|
||||||
- WorkAdventure doesn't support object layers and will ignore them
|
|
||||||
- If you are starting from a blank map, your map MUST be orthogonal and tiles size should be 32x32.
|
|
||||||
|
|
||||||
![](doc/images/tiled_screenshot_1.png)
|
|
||||||
|
|
||||||
### Defining a default entry point
|
|
||||||
|
|
||||||
In order to define a default start position, you MUST create a layer named "start" on your map.
|
|
||||||
This layer MUST contain at least one tile. The players will start on the tile of this layer.
|
|
||||||
If the layer contains many tiles selected, the players will start randomly on one of those tiles.
|
|
||||||
|
|
||||||
### Defining exits
|
|
||||||
|
|
||||||
In order to place an exit on your scene that leads to another scene:
|
|
||||||
|
|
||||||
- You must create a specific layer. When a character reaches ANY tile of that layer, it will exit the scene.
|
|
||||||
- In layer properties, you MUST add "exitSceneUrl" property. It represents the map URL of the next scene. For example : `/<map folder>/<map>.json`. Be careful, if you want the next map to be correctly loaded, you must check that the map files are in folder `back/src/Assets/Maps/<your map folder>`. The files will be accessible by url `<HOST>/map/files/<your map folder>/...`.
|
|
||||||
- In layer properties, you CAN add an "exitInstance" property. If set, you will join the map of the specified instance. Otherwise, you will stay on the same instance.
|
|
||||||
- If you want to have multiple exits, you can create many layers with name "exit". Each layer has a different key `exitSceneUrl` and have tiles that represent exits to another scene.
|
|
||||||
|
|
||||||
![](doc/images/exit_layer_map.png)
|
|
||||||
|
|
||||||
### Defining several entry points
|
|
||||||
|
|
||||||
Often your map will have several exits, and therefore, several entry points. For instance, if there
|
|
||||||
is an exit by a door that leads to the garden map, when you come back from the garden you expect to
|
|
||||||
come back by the same door. Therefore, a map can have several entry points.
|
|
||||||
Those entry points are "named" (they have a name).
|
|
||||||
|
|
||||||
In order to create a named entry point:
|
|
||||||
|
|
||||||
- You must create a specific layer. When a character enters the map by this entry point, it will enter the map randomly on ANY tile of that layer.
|
|
||||||
- In layer properties, you MUST add a boolean "startLayer" property. It should be set to true.
|
|
||||||
- The name of the entry point is the name of the layer
|
|
||||||
- To enter via this entry point, simply add a hash with the entry point name to the URL ("#[*startLayerName*]"). For instance: "https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point".
|
|
||||||
- You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL)
|
|
||||||
|
|
||||||
|
|
||||||
### MacOS developers, your environment with Vagrant
|
### MacOS developers, your environment with Vagrant
|
||||||
|
|
||||||
If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c).
|
If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c).
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
FROM thecodingmachine/workadventure-back-base:latest as builder
|
||||||
|
WORKDIR /var/www/messages
|
||||||
|
COPY --chown=docker:docker messages .
|
||||||
|
RUN yarn install && yarn proto
|
||||||
|
|
||||||
FROM thecodingmachine/nodejs:12
|
FROM thecodingmachine/nodejs:12
|
||||||
|
|
||||||
COPY --chown=docker:docker . .
|
COPY --chown=docker:docker back .
|
||||||
|
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"dev": "ts-node-dev --respawn --transpileOnly ./server.ts",
|
"dev": "ts-node-dev --respawn ./server.ts",
|
||||||
"prod": "tsc && node ./dist/server.js",
|
"prod": "tsc && node --max-old-space-size=4096 ./dist/server.js",
|
||||||
"profile": "tsc && node --prof ./dist/server.js",
|
"profile": "tsc && node --prof ./dist/server.js",
|
||||||
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||||
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
||||||
|
@ -36,25 +36,34 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/express": "^4.17.4",
|
"axios": "^0.20.0",
|
||||||
"@types/http-status-codes": "^1.2.0",
|
|
||||||
"@types/jsonwebtoken": "^8.3.8",
|
|
||||||
"@types/socket.io": "^2.1.4",
|
|
||||||
"@types/uuidv4": "^5.0.0",
|
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"express": "^4.17.1",
|
"busboy": "^0.3.1",
|
||||||
|
"circular-json": "^0.5.9",
|
||||||
"generic-type-guard": "^3.2.0",
|
"generic-type-guard": "^3.2.0",
|
||||||
|
"google-protobuf": "^3.13.0",
|
||||||
"http-status-codes": "^1.4.0",
|
"http-status-codes": "^1.4.0",
|
||||||
|
"iterall": "^1.3.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"mkdirp": "^1.0.4",
|
||||||
|
"multer": "^1.4.2",
|
||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
"socket.io": "^2.3.0",
|
"query-string": "^6.13.3",
|
||||||
"systeminformation": "^4.26.5",
|
"systeminformation": "^4.27.11",
|
||||||
"ts-node-dev": "^1.0.0-pre.44",
|
"ts-node-dev": "^1.0.0-pre.44",
|
||||||
"typescript": "^3.8.3",
|
"typescript": "^3.8.3",
|
||||||
|
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||||
"uuidv4": "^6.0.7"
|
"uuidv4": "^6.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/busboy": "^0.2.3",
|
||||||
|
"@types/circular-json": "^0.4.0",
|
||||||
|
"@types/google-protobuf": "^3.7.3",
|
||||||
|
"@types/http-status-codes": "^1.2.0",
|
||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.10",
|
||||||
|
"@types/jsonwebtoken": "^8.3.8",
|
||||||
|
"@types/mkdirp": "^1.0.1",
|
||||||
|
"@types/uuidv4": "^5.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
"@typescript-eslint/parser": "^2.26.0",
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
// lib/server.ts
|
// lib/server.ts
|
||||||
import App from "./src/App";
|
import App from "./src/App";
|
||||||
App.listen(8080, () => console.log(`Example app listening on port 8080!`))
|
App.listen(8080, () => console.log(`WorkAdventure starting on port 8080!`))
|
||||||
|
|
|
@ -1,55 +1,32 @@
|
||||||
// lib/app.ts
|
// lib/app.ts
|
||||||
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
|
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
|
||||||
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
|
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
|
||||||
import express from "express";
|
|
||||||
import {Application, Request, Response} from 'express';
|
|
||||||
import bodyParser = require('body-parser');
|
|
||||||
import * as http from "http";
|
|
||||||
import {MapController} from "./Controller/MapController";
|
import {MapController} from "./Controller/MapController";
|
||||||
import {PrometheusController} from "./Controller/PrometheusController";
|
import {PrometheusController} from "./Controller/PrometheusController";
|
||||||
|
import {FileController} from "./Controller/FileController";
|
||||||
|
import {DebugController} from "./Controller/DebugController";
|
||||||
|
import {App as uwsApp} from "./Server/sifrr.server";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public app: Application;
|
public app: uwsApp;
|
||||||
public server: http.Server;
|
|
||||||
public ioSocketController: IoSocketController;
|
public ioSocketController: IoSocketController;
|
||||||
public authenticateController: AuthenticateController;
|
public authenticateController: AuthenticateController;
|
||||||
|
public fileController: FileController;
|
||||||
public mapController: MapController;
|
public mapController: MapController;
|
||||||
public prometheusController: PrometheusController;
|
public prometheusController: PrometheusController;
|
||||||
|
private debugController: DebugController;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express();
|
this.app = new uwsApp();
|
||||||
|
|
||||||
//config server http
|
|
||||||
this.server = http.createServer(this.app);
|
|
||||||
|
|
||||||
this.config();
|
|
||||||
this.crossOrigin();
|
|
||||||
|
|
||||||
//TODO add middleware with access token to secure api
|
|
||||||
|
|
||||||
//create socket controllers
|
//create socket controllers
|
||||||
this.ioSocketController = new IoSocketController(this.server);
|
this.ioSocketController = new IoSocketController(this.app);
|
||||||
this.authenticateController = new AuthenticateController(this.app);
|
this.authenticateController = new AuthenticateController(this.app);
|
||||||
|
this.fileController = new FileController(this.app);
|
||||||
this.mapController = new MapController(this.app);
|
this.mapController = new MapController(this.app);
|
||||||
this.prometheusController = new PrometheusController(this.app, this.ioSocketController);
|
this.prometheusController = new PrometheusController(this.app);
|
||||||
}
|
this.debugController = new DebugController(this.app);
|
||||||
|
|
||||||
// TODO add session user
|
|
||||||
private config(): void {
|
|
||||||
this.app.use(bodyParser.json());
|
|
||||||
this.app.use(bodyParser.urlencoded({extended: false}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private crossOrigin(){
|
|
||||||
this.app.use((req: Request, res: Response, next) => {
|
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*"); // update to match the domain you will make the request from
|
|
||||||
// Request methods you wish to allow
|
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
|
||||||
// Request headers you wish to allow
|
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new App().server;
|
export default new App().app;
|
||||||
|
|
|
@ -1,40 +1,135 @@
|
||||||
import {Application, Request, Response} from "express";
|
import { v4 } from 'uuid';
|
||||||
import Jwt from "jsonwebtoken";
|
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
|
||||||
import {BAD_REQUEST, OK} from "http-status-codes";
|
import {BaseController} from "./BaseController";
|
||||||
import {SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
import {adminApi} from "../Services/AdminApi";
|
||||||
import { uuid } from 'uuidv4';
|
import {jwtTokenManager} from "../Services/JWTTokenManager";
|
||||||
|
import {parse} from "query-string";
|
||||||
|
|
||||||
export interface TokenInterface {
|
export interface TokenInterface {
|
||||||
name: string,
|
userUuid: string
|
||||||
userId: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthenticateController {
|
export class AuthenticateController extends BaseController {
|
||||||
App : Application;
|
|
||||||
|
constructor(private App : TemplatedApp) {
|
||||||
|
super();
|
||||||
|
this.register();
|
||||||
|
this.verify();
|
||||||
|
this.anonymLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Try to login with an admin token
|
||||||
|
private register(){
|
||||||
|
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.App.post("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
(async () => {
|
||||||
|
res.onAborted(() => {
|
||||||
|
console.warn('Login request was aborted');
|
||||||
|
})
|
||||||
|
const param = await res.json();
|
||||||
|
|
||||||
|
//todo: what to do if the organizationMemberToken is already used?
|
||||||
|
const organizationMemberToken:string|null = param.organizationMemberToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof organizationMemberToken != 'string') throw new Error('No organization token');
|
||||||
|
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
||||||
|
const userUuid = data.userUuid;
|
||||||
|
const organizationSlug = data.organizationSlug;
|
||||||
|
const worldSlug = data.worldSlug;
|
||||||
|
const roomSlug = data.roomSlug;
|
||||||
|
const mapUrlStart = data.mapUrlStart;
|
||||||
|
const textures = data.textures;
|
||||||
|
|
||||||
|
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
||||||
|
res.writeStatus("200 OK");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
authToken,
|
||||||
|
userUuid,
|
||||||
|
organizationSlug,
|
||||||
|
worldSlug,
|
||||||
|
roomSlug,
|
||||||
|
mapUrlStart,
|
||||||
|
textures
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("An error happened", e)
|
||||||
|
res.writeStatus(e.status || "500 Internal Server Error");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end('An error happened');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private verify(){
|
||||||
|
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
(async () => {
|
||||||
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
|
res.onAborted(() => {
|
||||||
|
console.warn('verify request was aborted');
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await jwtTokenManager.getUserUuidFromToken(query.token as string);
|
||||||
|
} catch (e) {
|
||||||
|
res.writeStatus("400 Bad Request");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
"success": false,
|
||||||
|
"message": "Invalid JWT token"
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeStatus("200 OK");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
"success": true
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
constructor(App : Application) {
|
|
||||||
this.App = App;
|
|
||||||
this.login();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//permit to login on application. Return token to connect on Websocket IO.
|
//permit to login on application. Return token to connect on Websocket IO.
|
||||||
login(){
|
private anonymLogin(){
|
||||||
// For now, let's completely forget the /login route.
|
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||||
this.App.post("/login", (req: Request, res: Response) => {
|
this.addCorsHeaders(res);
|
||||||
const param = req.body;
|
|
||||||
/*if(!param.name){
|
res.end();
|
||||||
return res.status(BAD_REQUEST).send({
|
});
|
||||||
message: "email parameter is empty"
|
|
||||||
});
|
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||||
}*/
|
|
||||||
//TODO check user email for The Coding Machine game
|
res.onAborted(() => {
|
||||||
const userId = uuid();
|
console.warn('Login request was aborted');
|
||||||
const token = Jwt.sign({name: param.name, userId: userId} as TokenInterface, SECRET_KEY, {expiresIn: '24h'});
|
})
|
||||||
return res.status(OK).send({
|
|
||||||
token: token,
|
const userUuid = v4();
|
||||||
mapUrlStart: URL_ROOM_STARTED,
|
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
||||||
userId: userId,
|
res.writeStatus("200 OK");
|
||||||
});
|
this.addCorsHeaders(res);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
authToken,
|
||||||
|
userUuid,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
back/src/Controller/BaseController.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||||
|
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
|
||||||
|
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', '*');
|
||||||
|
}
|
||||||
|
}
|
45
back/src/Controller/DebugController.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
||||||
|
import {IoSocketController} from "_Controller/IoSocketController";
|
||||||
|
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) {
|
||||||
|
this.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.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify(
|
||||||
|
socketManager.getWorlds(),
|
||||||
|
(key: unknown, value: unknown) => {
|
||||||
|
if(value instanceof Map) {
|
||||||
|
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
for (const [mapKey, mapValue] of value.entries()) {
|
||||||
|
obj[mapKey] = mapValue;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
} else if(value instanceof Set) {
|
||||||
|
const obj: Array<unknown> = [];
|
||||||
|
for (const [setKey, setValue] of value.entries()) {
|
||||||
|
obj.push(setValue);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
161
back/src/Controller/FileController.ts
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import {App} from "../Server/sifrr.server";
|
||||||
|
|
||||||
|
import {v4} from "uuid";
|
||||||
|
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||||
|
import {BaseController} from "./BaseController";
|
||||||
|
import { Readable } from 'stream'
|
||||||
|
|
||||||
|
interface UploadedFileBuffer {
|
||||||
|
buffer: Buffer,
|
||||||
|
expireDate: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileController extends BaseController {
|
||||||
|
private uploadedFileBuffers: Map<string, UploadedFileBuffer> = new Map<string, UploadedFileBuffer>();
|
||||||
|
|
||||||
|
constructor(private App : App) {
|
||||||
|
super();
|
||||||
|
this.App = App;
|
||||||
|
this.uploadAudioMessage();
|
||||||
|
this.downloadAudioMessage();
|
||||||
|
|
||||||
|
// Cleanup every 1 minute
|
||||||
|
setInterval(this.cleanup.bind(this), 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean memory from old files
|
||||||
|
*/
|
||||||
|
cleanup(): void {
|
||||||
|
const now = new Date();
|
||||||
|
for (const [id, file] of this.uploadedFileBuffers) {
|
||||||
|
if (file.expireDate < now) {
|
||||||
|
this.uploadedFileBuffers.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadAudioMessage(){
|
||||||
|
this.App.options("/upload-audio-message", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.App.post("/upload-audio-message", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
(async () => {
|
||||||
|
res.onAborted(() => {
|
||||||
|
console.warn('upload-audio-message request was aborted');
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const audioMessageId = v4();
|
||||||
|
|
||||||
|
const params = await res.formData({
|
||||||
|
onFile: (fieldname: string,
|
||||||
|
file: NodeJS.ReadableStream,
|
||||||
|
filename: string,
|
||||||
|
encoding: string,
|
||||||
|
mimetype: string) => {
|
||||||
|
(async () => {
|
||||||
|
console.log('READING FILE', fieldname)
|
||||||
|
|
||||||
|
const chunks: Buffer[] = []
|
||||||
|
for await (const chunk of file) {
|
||||||
|
if (!(chunk instanceof Buffer)) {
|
||||||
|
throw new Error('Unexpected chunk');
|
||||||
|
}
|
||||||
|
chunks.push(chunk)
|
||||||
|
}
|
||||||
|
// Let's expire in 1 minute.
|
||||||
|
const expireDate = new Date();
|
||||||
|
expireDate.setMinutes(expireDate.getMinutes() + 1);
|
||||||
|
this.uploadedFileBuffers.set(audioMessageId, {
|
||||||
|
buffer: Buffer.concat(chunks),
|
||||||
|
expireDate
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeStatus("200 OK");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
id: audioMessageId,
|
||||||
|
path: `/download-audio-message/${audioMessageId}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.log("An error happened", e)
|
||||||
|
res.writeStatus(e.status || "500 Internal Server Error");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end('An error happened');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadAudioMessage(){
|
||||||
|
this.App.options("/download-audio-message/*", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.App.get("/download-audio-message/:id", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
|
||||||
|
res.onAborted(() => {
|
||||||
|
console.warn('upload-audio-message request was aborted');
|
||||||
|
})
|
||||||
|
|
||||||
|
const id = req.getParameter(0);
|
||||||
|
|
||||||
|
const file = this.uploadedFileBuffers.get(id);
|
||||||
|
if (file === undefined) {
|
||||||
|
res.writeStatus("404 Not found");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end("Cannot find file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readable = new Readable()
|
||||||
|
readable._read = () => {} // _read is required but you can noop it
|
||||||
|
readable.push(file.buffer);
|
||||||
|
readable.push(null);
|
||||||
|
|
||||||
|
const size = file.buffer.byteLength;
|
||||||
|
|
||||||
|
res.writeStatus("200 OK");
|
||||||
|
|
||||||
|
readable.on('data', buffer => {
|
||||||
|
const chunk = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
|
||||||
|
lastOffset = res.getWriteOffset();
|
||||||
|
|
||||||
|
// First try
|
||||||
|
const [ok, done] = res.tryEnd(chunk, size);
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
readable.destroy();
|
||||||
|
} else if (!ok) {
|
||||||
|
// pause because backpressure
|
||||||
|
readable.pause();
|
||||||
|
|
||||||
|
// Save unsent chunk for later
|
||||||
|
res.ab = chunk;
|
||||||
|
res.abOffset = lastOffset;
|
||||||
|
|
||||||
|
// Register async handlers for drainage
|
||||||
|
res.onWritable(offset => {
|
||||||
|
const [ok, done] = res.tryEnd(res.ab.slice(offset - res.abOffset), size);
|
||||||
|
if (done) {
|
||||||
|
readable.destroy();
|
||||||
|
} else if (ok) {
|
||||||
|
readable.resume();
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,431 +1,325 @@
|
||||||
import socketIO = require('socket.io');
|
import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||||
import {Socket} from "socket.io";
|
import {GameRoomPolicyTypes} from "../Model/GameRoom";
|
||||||
import * as http from "http";
|
import {PointInterface} from "../Model/Websocket/PointInterface";
|
||||||
import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
|
import {
|
||||||
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
SetPlayerDetailsMessage,
|
||||||
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
SubMessage,
|
||||||
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
BatchMessage,
|
||||||
import {World} from "../Model/World";
|
ItemEventMessage,
|
||||||
import {Group} from "_Model/Group";
|
ViewportMessage,
|
||||||
import {UserInterface} from "_Model/UserInterface";
|
ClientToServerMessage,
|
||||||
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
|
SilentMessage,
|
||||||
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
WebRtcSignalToServerMessage,
|
||||||
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
PlayGlobalMessage,
|
||||||
import si from "systeminformation";
|
ReportPlayerMessage,
|
||||||
import {Gauge} from "prom-client";
|
QueryJitsiJwtMessage
|
||||||
import os from 'os';
|
} from "../Messages/generated/messages_pb";
|
||||||
import {TokenInterface} from "../Controller/AuthenticateController";
|
import {UserMovesMessage} from "../Messages/generated/messages_pb";
|
||||||
import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
|
import {TemplatedApp} from "uWebSockets.js"
|
||||||
import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface";
|
import {parse} from "query-string";
|
||||||
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
import {jwtTokenManager} from "../Services/JWTTokenManager";
|
||||||
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi";
|
||||||
|
import {SocketManager, socketManager} from "../Services/SocketManager";
|
||||||
enum SockerIoEvent {
|
import {emitInBatch, resetPing} from "../Services/IoSocketHelpers";
|
||||||
CONNECTION = "connection",
|
import {clientEventsEmitter} from "../Services/ClientEventsEmitter";
|
||||||
DISCONNECT = "disconnect",
|
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
||||||
JOIN_ROOM = "join-room", // bi-directional
|
|
||||||
USER_POSITION = "user-position", // bi-directional
|
|
||||||
USER_MOVED = "user-moved", // From server to client
|
|
||||||
USER_LEFT = "user-left", // From server to client
|
|
||||||
WEBRTC_SIGNAL = "webrtc-signal",
|
|
||||||
WEBRTC_START = "webrtc-start",
|
|
||||||
WEBRTC_DISCONNECT = "webrtc-disconect",
|
|
||||||
MESSAGE_ERROR = "message-error",
|
|
||||||
GROUP_CREATE_UPDATE = "group-create-update",
|
|
||||||
GROUP_DELETE = "group-delete",
|
|
||||||
SET_PLAYER_DETAILS = "set-player-details"
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IoSocketController {
|
export class IoSocketController {
|
||||||
public readonly Io: socketIO.Server;
|
private nextUserId: number = 1;
|
||||||
private Worlds: Map<string, World> = new Map<string, World>();
|
|
||||||
private sockets: Map<string, ExSocketInterface> = new Map<string, ExSocketInterface>();
|
|
||||||
private nbClientsGauge: Gauge<string>;
|
|
||||||
private nbClientsPerRoomGauge: Gauge<string>;
|
|
||||||
|
|
||||||
constructor(server: http.Server) {
|
|
||||||
this.Io = socketIO(server);
|
|
||||||
this.nbClientsGauge = new Gauge({
|
|
||||||
name: 'workadventure_nb_sockets',
|
|
||||||
help: 'Number of connected sockets',
|
|
||||||
labelNames: [ 'host' ]
|
|
||||||
});
|
|
||||||
this.nbClientsPerRoomGauge = new Gauge({
|
|
||||||
name: 'workadventure_nb_clients_per_room',
|
|
||||||
help: 'Number of clients per room',
|
|
||||||
labelNames: [ 'host', 'room' ]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Authentication with token. it will be decoded and stored in the socket.
|
|
||||||
// Completely commented for now, as we do not use the "/login" route at all.
|
|
||||||
this.Io.use((socket: Socket, next) => {
|
|
||||||
if (!socket.handshake.query || !socket.handshake.query.token) {
|
|
||||||
console.error('An authentication error happened, a user tried to connect without a token.');
|
|
||||||
return next(new Error('Authentication error'));
|
|
||||||
}
|
|
||||||
if(this.searchClientByToken(socket.handshake.query.token)){
|
|
||||||
console.error('An authentication error happened, a user tried to connect while its token is already connected.');
|
|
||||||
return next(new Error('Authentication error'));
|
|
||||||
}
|
|
||||||
Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('An authentication error happened, invalid JsonWebToken.', err);
|
|
||||||
return next(new Error('Authentication error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isValidToken(tokenDecoded)) {
|
|
||||||
return next(new Error('Authentication error, invalid token structure'));
|
|
||||||
}
|
|
||||||
|
|
||||||
(socket as ExSocketInterface).token = socket.handshake.query.token;
|
|
||||||
(socket as ExSocketInterface).userId = tokenDecoded.userId;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
constructor(private readonly app: TemplatedApp) {
|
||||||
this.ioConnection();
|
this.ioConnection();
|
||||||
|
this.adminRoomSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
private isValidToken(token: object): token is TokenInterface {
|
adminRoomSocket() {
|
||||||
if (typeof((token as TokenInterface).userId) !== 'string') {
|
this.app.ws('/admin/rooms', {
|
||||||
return false;
|
upgrade: (res, req, context) => {
|
||||||
}
|
const query = parse(req.getQuery());
|
||||||
if (typeof((token as TokenInterface).name) !== 'string') {
|
const websocketKey = req.getHeader('sec-websocket-key');
|
||||||
return false;
|
const websocketProtocol = req.getHeader('sec-websocket-protocol');
|
||||||
}
|
const websocketExtensions = req.getHeader('sec-websocket-extensions');
|
||||||
return true;
|
const token = query.token;
|
||||||
}
|
if (token !== ADMIN_API_TOKEN) {
|
||||||
|
console.log('Admin access refused for token: '+token)
|
||||||
|
res.writeStatus("401 Unauthorized").end('Incorrect token');
|
||||||
|
}
|
||||||
|
const roomId = query.roomId as string;
|
||||||
|
|
||||||
/**
|
res.upgrade(
|
||||||
*
|
{roomId},
|
||||||
* @param token
|
websocketKey, websocketProtocol, websocketExtensions, context,
|
||||||
*/
|
);
|
||||||
searchClientByToken(token: string): ExSocketInterface | null {
|
},
|
||||||
const clients: ExSocketInterface[] = Object.values(this.Io.sockets.sockets) as ExSocketInterface[];
|
open: (ws) => {
|
||||||
for (let i = 0; i < clients.length; i++) {
|
console.log('Admin socket connect for room: '+ws.roomId);
|
||||||
const client = clients[i];
|
ws.send('Data:'+JSON.stringify(socketManager.getAdminSocketDataFor(ws.roomId as string)));
|
||||||
if (client.token !== token) {
|
ws.clientJoinCallback = (clientUUid: string, roomId: string) => {
|
||||||
continue
|
const wsroomId = ws.roomId as string;
|
||||||
|
if(wsroomId === roomId) {
|
||||||
|
ws.send('MemberJoin:'+clientUUid+';'+roomId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.clientLeaveCallback = (clientUUid: string, roomId: string) => {
|
||||||
|
const wsroomId = ws.roomId as string;
|
||||||
|
if(wsroomId === roomId) {
|
||||||
|
ws.send('MemberLeave:'+clientUUid+';'+roomId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
clientEventsEmitter.registerToClientJoin(ws.clientJoinCallback);
|
||||||
|
clientEventsEmitter.registerToClientLeave(ws.clientLeaveCallback);
|
||||||
|
},
|
||||||
|
message: (ws, arrayBuffer, isBinary): void => {
|
||||||
|
try {
|
||||||
|
//TODO refactor message type and data
|
||||||
|
const message: {event: string, message: {type: string, message: unknown, userUuid: string}} =
|
||||||
|
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
|
||||||
|
|
||||||
|
if(message.event === 'user-message') {
|
||||||
|
const messageToEmit = (message.message as { message: string, type: string, userUuid: string });
|
||||||
|
switch (message.message.type) {
|
||||||
|
case 'ban': {
|
||||||
|
socketManager.emitSendUserMessage(messageToEmit);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'banned': {
|
||||||
|
const socketUser = socketManager.emitSendUserMessage(messageToEmit);
|
||||||
|
setTimeout(() => {
|
||||||
|
socketUser.close();
|
||||||
|
}, 10000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close: (ws, code, message) => {
|
||||||
|
//todo make sure this code unregister the right listeners
|
||||||
|
clientEventsEmitter.unregisterFromClientJoin(ws.clientJoinCallback);
|
||||||
|
clientEventsEmitter.unregisterFromClientLeave(ws.clientLeaveCallback);
|
||||||
}
|
}
|
||||||
return client;
|
})
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendUpdateGroupEvent(group: Group): void {
|
|
||||||
// Let's get the room of the group. To do this, let's get anyone in the group and find its room.
|
|
||||||
// Note: this is suboptimal
|
|
||||||
const userId = group.getUsers()[0].id;
|
|
||||||
const client: ExSocketInterface = this.searchClientByIdOrFail(userId);
|
|
||||||
const roomId = client.roomId;
|
|
||||||
this.Io.in(roomId).emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
|
|
||||||
position: group.getPosition(),
|
|
||||||
groupId: group.getId()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendDeleteGroupEvent(uuid: string, lastUser: UserInterface): void {
|
|
||||||
// Let's get the room of the group. To do this, let's get anyone in the group and find its room.
|
|
||||||
const userId = lastUser.id;
|
|
||||||
const client: ExSocketInterface = this.searchClientByIdOrFail(userId);
|
|
||||||
const roomId = client.roomId;
|
|
||||||
this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ioConnection() {
|
ioConnection() {
|
||||||
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
|
this.app.ws('/room', {
|
||||||
const client : ExSocketInterface = socket as ExSocketInterface;
|
/* Options */
|
||||||
this.sockets.set(client.userId, client);
|
//compression: uWS.SHARED_COMPRESSOR,
|
||||||
|
maxPayloadLength: 16 * 1024 * 1024,
|
||||||
|
maxBackpressure: 65536, // Maximum 64kB of data in the buffer.
|
||||||
|
//idleTimeout: 10,
|
||||||
|
upgrade: (res, req, context) => {
|
||||||
|
//console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!');
|
||||||
|
(async () => {
|
||||||
|
/* Keep track of abortions */
|
||||||
|
const upgradeAborted = {aborted: false};
|
||||||
|
|
||||||
// Let's log server load when a user joins
|
res.onAborted(() => {
|
||||||
const srvSockets = this.Io.sockets.sockets;
|
/* We can simply signal that we were aborted */
|
||||||
this.nbClientsGauge.inc({ host: os.hostname() });
|
upgradeAborted.aborted = true;
|
||||||
console.log(new Date().toISOString() + ' A user joined (', Object.keys(srvSockets).length, ' connected users)');
|
|
||||||
si.currentLoad().then(data => console.log(' Current load: ', data.avgload));
|
|
||||||
si.currentLoad().then(data => console.log(' CPU: ', data.currentload, '%'));
|
|
||||||
// End log server load
|
|
||||||
|
|
||||||
/*join-rom event permit to join one room.
|
|
||||||
message :
|
|
||||||
userId : user identification
|
|
||||||
roomId: room identification
|
|
||||||
position: position of user in map
|
|
||||||
x: user x position on map
|
|
||||||
y: user y position on map
|
|
||||||
*/
|
|
||||||
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
|
||||||
try {
|
|
||||||
if (!isJoinRoomMessageInterface(message)) {
|
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM message.'});
|
|
||||||
console.warn('Invalid JOIN_ROOM message received: ', message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const roomId = message.roomId;
|
|
||||||
|
|
||||||
const Client = (socket as ExSocketInterface);
|
|
||||||
|
|
||||||
if (Client.roomId === roomId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//leave previous room
|
|
||||||
this.leaveRoom(Client);
|
|
||||||
|
|
||||||
//join new previous room
|
|
||||||
const world = this.joinRoom(Client, roomId, message.position);
|
|
||||||
|
|
||||||
//add function to refresh position user in real time.
|
|
||||||
//this.refreshUserPosition(Client);
|
|
||||||
|
|
||||||
const messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.character, Client.position);
|
|
||||||
|
|
||||||
socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserJoined);
|
|
||||||
|
|
||||||
// The answer shall contain the list of all users of the room with their positions:
|
|
||||||
const listOfUsers = Array.from(world.getUsers(), ([key, user]) => {
|
|
||||||
const player = this.searchClientByIdOrFail(user.id);
|
|
||||||
return new MessageUserPosition(user.id, player.name, player.character, player.position);
|
|
||||||
});
|
});
|
||||||
answerFn(listOfUsers);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "join_room" event');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => {
|
try {
|
||||||
try {
|
const url = req.getUrl();
|
||||||
if (!isPointInterface(position)) {
|
const query = parse(req.getQuery());
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
const websocketKey = req.getHeader('sec-websocket-key');
|
||||||
console.warn('Invalid USER_POSITION message received: ', position);
|
const websocketProtocol = req.getHeader('sec-websocket-protocol');
|
||||||
|
const websocketExtensions = req.getHeader('sec-websocket-extensions');
|
||||||
|
|
||||||
|
const roomId = query.roomId;
|
||||||
|
if (typeof roomId !== 'string') {
|
||||||
|
throw new Error('Undefined room ID: ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = query.token;
|
||||||
|
const x = Number(query.x);
|
||||||
|
const y = Number(query.y);
|
||||||
|
const top = Number(query.top);
|
||||||
|
const bottom = Number(query.bottom);
|
||||||
|
const left = Number(query.left);
|
||||||
|
const right = Number(query.right);
|
||||||
|
const name = query.name;
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
throw new Error('Expecting name');
|
||||||
|
}
|
||||||
|
if (name === '') {
|
||||||
|
throw new Error('No empty name');
|
||||||
|
}
|
||||||
|
let characterLayers = query.characterLayers;
|
||||||
|
if (characterLayers === null) {
|
||||||
|
throw new Error('Expecting skin');
|
||||||
|
}
|
||||||
|
if (typeof characterLayers === 'string') {
|
||||||
|
characterLayers = [ characterLayers ];
|
||||||
|
}
|
||||||
|
|
||||||
|
const userUuid = await jwtTokenManager.getUserUuidFromToken(token);
|
||||||
|
|
||||||
|
let memberTags: string[] = [];
|
||||||
|
let memberTextures: CharacterTexture[] = [];
|
||||||
|
const room = await socketManager.getOrCreateRoom(roomId);
|
||||||
|
if(room.isFull){
|
||||||
|
throw new Error('Room is full');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const userData = await adminApi.fetchMemberDataByUuid(userUuid);
|
||||||
|
//console.log('USERDATA', userData)
|
||||||
|
memberTags = userData.tags;
|
||||||
|
memberTextures = userData.textures;
|
||||||
|
if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) {
|
||||||
|
throw new Error('No correct tags')
|
||||||
|
}
|
||||||
|
//console.log('access granted for user '+userUuid+' and room '+roomId);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('access not granted for user '+userUuid+' and room '+roomId);
|
||||||
|
throw new Error('Client cannot acces this ressource.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate characterLayers objects from characterLayers string[]
|
||||||
|
const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
|
||||||
|
|
||||||
|
if (upgradeAborted.aborted) {
|
||||||
|
console.log("Ouch! Client disconnected before we could upgrade it!");
|
||||||
|
/* You must not upgrade now */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This immediately calls open handler, you must not use res after this call */
|
||||||
|
res.upgrade({
|
||||||
|
// Data passed here is accessible on the "websocket" socket object.
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
userUuid,
|
||||||
|
roomId,
|
||||||
|
name,
|
||||||
|
characterLayers: characterLayerObjs,
|
||||||
|
tags: memberTags,
|
||||||
|
textures: memberTextures,
|
||||||
|
position: {
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
direction: 'down',
|
||||||
|
moving: false
|
||||||
|
} as PointInterface,
|
||||||
|
viewport: {
|
||||||
|
top,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
left
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* Spell these correctly */
|
||||||
|
websocketKey,
|
||||||
|
websocketProtocol,
|
||||||
|
websocketExtensions,
|
||||||
|
context);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.log(e.message);
|
||||||
|
res.writeStatus("401 Unauthorized").end(e.message);
|
||||||
|
} else {
|
||||||
|
console.log(e);
|
||||||
|
res.writeStatus("500 Internal Server Error").end('An error occurred');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
/* Handlers */
|
||||||
|
open: (ws) => {
|
||||||
|
// Let's join the room
|
||||||
|
const client = this.initClient(ws); //todo: into the upgrade instead?
|
||||||
|
socketManager.handleJoinRoom(client);
|
||||||
|
resetPing(client);
|
||||||
|
|
||||||
const Client = (socket as ExSocketInterface);
|
//get data information and shwo messages
|
||||||
|
adminApi.fetchMemberDataByUuid(client.userUuid).then((res: FetchMemberDataByUuidResponse) => {
|
||||||
// sending to all clients in room except sender
|
if (!res.messages) {
|
||||||
Client.position = position;
|
|
||||||
|
|
||||||
// update position in the world
|
|
||||||
const world = this.Worlds.get(Client.roomId);
|
|
||||||
if (!world) {
|
|
||||||
console.error("Could not find world with id '", Client.roomId, "'");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
world.updatePosition(Client, position);
|
res.messages.forEach((c: unknown) => {
|
||||||
|
const messageToSend = c as { type: string, message: string };
|
||||||
|
socketManager.emitSendUserMessage({
|
||||||
|
userUuid: client.userUuid,
|
||||||
|
type: messageToSend.type,
|
||||||
|
message: messageToSend.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('fetchMemberDataByUuid => err', err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
message: (ws, arrayBuffer, isBinary): void => {
|
||||||
|
const client = ws as ExSocketInterface;
|
||||||
|
const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer));
|
||||||
|
|
||||||
socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position));
|
if (message.hasViewportmessage()) {
|
||||||
} catch (e) {
|
socketManager.handleViewport(client, message.getViewportmessage() as ViewportMessage);
|
||||||
console.error('An error occurred on "user_position" event');
|
} else if (message.hasUsermovesmessage()) {
|
||||||
console.error(e);
|
socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
|
||||||
|
} else if (message.hasSetplayerdetailsmessage()) {
|
||||||
|
socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage);
|
||||||
|
} else if (message.hasSilentmessage()) {
|
||||||
|
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
|
||||||
|
} else if (message.hasItemeventmessage()) {
|
||||||
|
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
|
||||||
|
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||||
|
socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
|
||||||
|
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||||
|
socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
|
||||||
|
} else if (message.hasPlayglobalmessage()) {
|
||||||
|
socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage);
|
||||||
|
} else if (message.hasReportplayermessage()){
|
||||||
|
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
|
||||||
|
} else if (message.hasQueryjitsijwtmessage()){
|
||||||
|
socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: unknown) => {
|
/* Ok is false if backpressure was built up, wait for drain */
|
||||||
if (!isWebRtcSignalMessageInterface(data)) {
|
//let ok = ws.send(message, isBinary);
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid WEBRTC_SIGNAL message.'});
|
},
|
||||||
console.warn('Invalid WEBRTC_SIGNAL message received: ', data);
|
drain: (ws) => {
|
||||||
return;
|
console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
|
||||||
}
|
},
|
||||||
//send only at user
|
close: (ws, code, message) => {
|
||||||
const client = this.sockets.get(data.receiverId);
|
const Client = (ws as ExSocketInterface);
|
||||||
if (client === undefined) {
|
|
||||||
console.warn("While exchanging a WebRTC signal: client with id ", data.receiverId, " does not exist. This might be a race condition.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return client.emit(SockerIoEvent.WEBRTC_SIGNAL, data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on(SockerIoEvent.DISCONNECT, () => {
|
|
||||||
const Client = (socket as ExSocketInterface);
|
|
||||||
try {
|
try {
|
||||||
|
Client.disconnecting = true;
|
||||||
//leave room
|
//leave room
|
||||||
this.leaveRoom(Client);
|
socketManager.leaveRoom(Client);
|
||||||
|
|
||||||
//leave webrtc room
|
|
||||||
//socket.leave(Client.webRtcRoomId);
|
|
||||||
|
|
||||||
//delete all socket information
|
|
||||||
delete Client.webRtcRoomId;
|
|
||||||
delete Client.roomId;
|
|
||||||
delete Client.token;
|
|
||||||
delete Client.position;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('An error occurred on "disconnect"');
|
console.error('An error occurred on "disconnect"');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
this.sockets.delete(Client.userId);
|
}
|
||||||
|
})
|
||||||
// Let's log server load when a user leaves
|
|
||||||
const srvSockets = this.Io.sockets.sockets;
|
|
||||||
this.nbClientsGauge.dec({ host: os.hostname() });
|
|
||||||
console.log('A user left (', Object.keys(srvSockets).length, ' connected users)');
|
|
||||||
si.currentLoad().then(data => console.log('Current load: ', data.avgload));
|
|
||||||
si.currentLoad().then(data => console.log('CPU: ', data.currentload, '%'));
|
|
||||||
// End log server load
|
|
||||||
});
|
|
||||||
|
|
||||||
// Let's send the user id to the user
|
|
||||||
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
|
||||||
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
|
||||||
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const Client = (socket as ExSocketInterface);
|
|
||||||
Client.name = playerDetails.name;
|
|
||||||
Client.character = playerDetails.character;
|
|
||||||
answerFn(Client.userId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchClientByIdOrFail(userId: string): ExSocketInterface {
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const client: ExSocketInterface|undefined = this.sockets.get(userId);
|
private initClient(ws: any): ExSocketInterface {
|
||||||
if (client === undefined) {
|
const client : ExSocketInterface = ws;
|
||||||
throw new Error("Could not find user with id " + userId);
|
client.userId = this.nextUserId;
|
||||||
|
this.nextUserId++;
|
||||||
|
client.userUuid = ws.userUuid;
|
||||||
|
client.token = ws.token;
|
||||||
|
client.batchedMessages = new BatchMessage();
|
||||||
|
client.batchTimeout = null;
|
||||||
|
client.emitInBatch = (payload: SubMessage): void => {
|
||||||
|
emitInBatch(client, payload);
|
||||||
}
|
}
|
||||||
|
client.disconnecting = false;
|
||||||
|
|
||||||
|
client.name = ws.name;
|
||||||
|
client.tags = ws.tags;
|
||||||
|
client.textures = ws.textures;
|
||||||
|
client.characterLayers = ws.characterLayers;
|
||||||
|
client.roomId = ws.roomId;
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveRoom(Client : ExSocketInterface){
|
|
||||||
// leave previous room and world
|
|
||||||
if(Client.roomId){
|
|
||||||
Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId);
|
|
||||||
|
|
||||||
//user leave previous world
|
|
||||||
const world : World|undefined = this.Worlds.get(Client.roomId);
|
|
||||||
if(world){
|
|
||||||
world.leave(Client);
|
|
||||||
}
|
|
||||||
//user leave previous room
|
|
||||||
Client.leave(Client.roomId);
|
|
||||||
this.nbClientsPerRoomGauge.dec({ host: os.hostname(), room: Client.roomId });
|
|
||||||
delete Client.roomId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private joinRoom(Client : ExSocketInterface, roomId: string, position: PointInterface): World {
|
|
||||||
//join user in room
|
|
||||||
Client.join(roomId);
|
|
||||||
this.nbClientsPerRoomGauge.inc({ host: os.hostname(), room: roomId });
|
|
||||||
Client.roomId = roomId;
|
|
||||||
Client.position = position;
|
|
||||||
|
|
||||||
//check and create new world for a room
|
|
||||||
let world = this.Worlds.get(roomId)
|
|
||||||
if(world === undefined){
|
|
||||||
world = new World((user1: string, group: Group) => {
|
|
||||||
this.connectedUser(user1, group);
|
|
||||||
}, (user1: string, group: Group) => {
|
|
||||||
this.disConnectedUser(user1, group);
|
|
||||||
}, MINIMUM_DISTANCE, GROUP_RADIUS, (group: Group) => {
|
|
||||||
this.sendUpdateGroupEvent(group);
|
|
||||||
}, (groupUuid: string, lastUser: UserInterface) => {
|
|
||||||
this.sendDeleteGroupEvent(groupUuid, lastUser);
|
|
||||||
});
|
|
||||||
this.Worlds.set(roomId, world);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch groups position to newly connected user
|
|
||||||
world.getGroups().forEach((group: Group) => {
|
|
||||||
Client.emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
|
|
||||||
position: group.getPosition(),
|
|
||||||
groupId: group.getId()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
//join world
|
|
||||||
world.join(Client, Client.position);
|
|
||||||
return world;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param socket
|
|
||||||
* @param roomId
|
|
||||||
*/
|
|
||||||
joinWebRtcRoom(socket: ExSocketInterface, roomId: string) {
|
|
||||||
if (socket.webRtcRoomId === roomId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.join(roomId);
|
|
||||||
socket.webRtcRoomId = roomId;
|
|
||||||
//if two persons in room share
|
|
||||||
if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
|
|
||||||
.filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId);
|
|
||||||
//send start at one client to initialise offer webrtc
|
|
||||||
//send all users in room to create PeerConnection in front
|
|
||||||
clients.forEach((client: ExSocketInterface, index: number) => {
|
|
||||||
|
|
||||||
const clientsId = clients.reduce((tabs: Array<UserInGroupInterface>, clientId: ExSocketInterface, indexClientId: number) => {
|
|
||||||
if (!clientId.userId || clientId.userId === client.userId) {
|
|
||||||
return tabs;
|
|
||||||
}
|
|
||||||
tabs.push({
|
|
||||||
userId: clientId.userId,
|
|
||||||
name: clientId.name,
|
|
||||||
initiator: index <= indexClientId
|
|
||||||
});
|
|
||||||
return tabs;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
client.emit(SockerIoEvent.WEBRTC_START, {clients: clientsId, roomId: roomId});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** permit to share user position
|
|
||||||
** users position will send in event 'user-position'
|
|
||||||
** The data sent is an array with information for each user :
|
|
||||||
[
|
|
||||||
{
|
|
||||||
userId: <string>,
|
|
||||||
roomId: <string>,
|
|
||||||
position: {
|
|
||||||
x : <number>,
|
|
||||||
y : <number>,
|
|
||||||
direction: <string>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
**/
|
|
||||||
|
|
||||||
//connected user
|
|
||||||
connectedUser(userId: string, group: Group) {
|
|
||||||
/*let Client = this.sockets.get(userId);
|
|
||||||
if (Client === undefined) {
|
|
||||||
return;
|
|
||||||
}*/
|
|
||||||
const Client = this.searchClientByIdOrFail(userId);
|
|
||||||
this.joinWebRtcRoom(Client, group.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
//disconnect user
|
|
||||||
disConnectedUser(userId: string, group: Group) {
|
|
||||||
const Client = this.searchClientByIdOrFail(userId);
|
|
||||||
Client.to(group.getId()).emit(SockerIoEvent.WEBRTC_DISCONNECT, {
|
|
||||||
userId: userId
|
|
||||||
});
|
|
||||||
|
|
||||||
// Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection
|
|
||||||
// which will be shut for the other player).
|
|
||||||
// However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player,
|
|
||||||
// the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing).
|
|
||||||
// So we also send the disconnect event to the other player.
|
|
||||||
for (const user of group.getUsers()) {
|
|
||||||
Client.emit(SockerIoEvent.WEBRTC_DISCONNECT, {
|
|
||||||
userId: user.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//disconnect webrtc room
|
|
||||||
if(!Client.webRtcRoomId){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Client.leave(Client.webRtcRoomId);
|
|
||||||
delete Client.webRtcRoomId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,70 @@
|
||||||
import express from "express";
|
|
||||||
import {Application, Request, Response} from "express";
|
|
||||||
import {OK} from "http-status-codes";
|
import {OK} from "http-status-codes";
|
||||||
import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable";
|
import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable";
|
||||||
|
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
|
||||||
|
import {BaseController} from "./BaseController";
|
||||||
|
import {parse} from "query-string";
|
||||||
|
import {adminApi} from "../Services/AdminApi";
|
||||||
|
|
||||||
export class MapController {
|
//todo: delete this
|
||||||
App: Application;
|
export class MapController extends BaseController{
|
||||||
|
|
||||||
constructor(App: Application) {
|
constructor(private App : TemplatedApp) {
|
||||||
|
super();
|
||||||
this.App = App;
|
this.App = App;
|
||||||
this.getStartMap();
|
this.getMapUrl();
|
||||||
this.assetMaps();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assetMaps() {
|
|
||||||
this.App.use('/map/files', express.static('src/Assets/Maps'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a map mapping map name to file name of the map
|
// Returns a map mapping map name to file name of the map
|
||||||
getStartMap() {
|
getMapUrl() {
|
||||||
this.App.get("/start-map", (req: Request, res: Response) => {
|
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
|
||||||
res.status(OK).send({
|
this.addCorsHeaders(res);
|
||||||
mapUrlStart: req.headers.host + "/map/files" + URL_ROOM_STARTED,
|
|
||||||
startInstance: "global"
|
res.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.App.get("/map", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
|
||||||
|
res.onAborted(() => {
|
||||||
|
console.warn('/map request was aborted');
|
||||||
|
})
|
||||||
|
|
||||||
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
|
if (typeof query.organizationSlug !== 'string') {
|
||||||
|
console.error('Expected organizationSlug parameter');
|
||||||
|
res.writeStatus("400 Bad request");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end("Expected organizationSlug parameter");
|
||||||
|
}
|
||||||
|
if (typeof query.worldSlug !== 'string') {
|
||||||
|
console.error('Expected worldSlug parameter');
|
||||||
|
res.writeStatus("400 Bad request");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end("Expected worldSlug parameter");
|
||||||
|
}
|
||||||
|
if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) {
|
||||||
|
console.error('Expected only one roomSlug parameter');
|
||||||
|
res.writeStatus("400 Bad request");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end("Expected only one roomSlug parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined);
|
||||||
|
|
||||||
|
res.writeStatus("200 OK");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end(JSON.stringify(mapDetails));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message || e);
|
||||||
|
res.writeStatus("500 Internal Server Error")
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end("An error occurred");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import {Application, Request, Response} from "express";
|
import {App} from "../Server/sifrr.server";
|
||||||
import {IoSocketController} from "_Controller/IoSocketController";
|
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||||
const register = require('prom-client').register;
|
const register = require('prom-client').register;
|
||||||
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
|
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
|
||||||
|
|
||||||
export class PrometheusController {
|
export class PrometheusController {
|
||||||
constructor(private App: Application, private ioSocketController: IoSocketController) {
|
constructor(private App: App) {
|
||||||
collectDefaultMetrics({
|
collectDefaultMetrics({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
||||||
|
@ -13,8 +13,8 @@ export class PrometheusController {
|
||||||
this.App.get("/metrics", this.metrics.bind(this));
|
this.App.get("/metrics", this.metrics.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private metrics(req: Request, res: Response): void {
|
private metrics(res: HttpResponse, req: HttpRequest): void {
|
||||||
res.set('Content-Type', register.contentType);
|
res.writeHeader('Content-Type', register.contentType);
|
||||||
res.end(register.metrics());
|
res.end(register.metrics());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,26 @@ const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
||||||
const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
||||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
||||||
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
||||||
|
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
|
||||||
|
const ADMIN_API_URL = process.env.ADMIN_API_URL || 'http://admin';
|
||||||
|
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
|
||||||
|
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600;
|
||||||
|
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 || '';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SECRET_KEY,
|
SECRET_KEY,
|
||||||
URL_ROOM_STARTED,
|
URL_ROOM_STARTED,
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
GROUP_RADIUS
|
ADMIN_API_URL,
|
||||||
|
ADMIN_API_TOKEN,
|
||||||
|
MAX_USERS_PER_ROOM,
|
||||||
|
GROUP_RADIUS,
|
||||||
|
ALLOW_ARTILLERY,
|
||||||
|
CPU_OVERHEAT_THRESHOLD,
|
||||||
|
JITSI_URL,
|
||||||
|
JITSI_ISS,
|
||||||
|
SECRET_JITSI_KEY
|
||||||
}
|
}
|
||||||
|
|
1
back/src/Messages/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/generated/
|
297
back/src/Model/GameRoom.ts
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
import {PointInterface} from "./Websocket/PointInterface";
|
||||||
|
import {Group} from "./Group";
|
||||||
|
import {User} from "./User";
|
||||||
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
import {Identificable} from "_Model/Websocket/Identificable";
|
||||||
|
import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
|
||||||
|
import {PositionNotifier} from "./PositionNotifier";
|
||||||
|
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||||
|
import {Movable} from "_Model/Movable";
|
||||||
|
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
|
||||||
|
import {arrayIntersect} from "../Services/ArrayHelper";
|
||||||
|
import {MAX_USERS_PER_ROOM} from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
|
|
||||||
|
export enum GameRoomPolicyTypes {
|
||||||
|
ANONYMUS_POLICY = 1,
|
||||||
|
MEMBERS_ONLY_POLICY,
|
||||||
|
USE_TAGS_POLICY,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameRoom {
|
||||||
|
private readonly minDistance: number;
|
||||||
|
private readonly groupRadius: number;
|
||||||
|
|
||||||
|
// Users, sorted by ID
|
||||||
|
private readonly users: Map<number, User>;
|
||||||
|
private readonly groups: Set<Group>;
|
||||||
|
|
||||||
|
private readonly connectCallback: ConnectCallback;
|
||||||
|
private readonly disconnectCallback: DisconnectCallback;
|
||||||
|
|
||||||
|
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||||
|
|
||||||
|
private readonly positionNotifier: PositionNotifier;
|
||||||
|
public readonly roomId: string;
|
||||||
|
public readonly anonymous: boolean;
|
||||||
|
public tags: string[];
|
||||||
|
public policyType: GameRoomPolicyTypes;
|
||||||
|
public readonly roomSlug: string;
|
||||||
|
public readonly worldSlug: string = '';
|
||||||
|
public readonly organizationSlug: string = '';
|
||||||
|
|
||||||
|
constructor(roomId: string,
|
||||||
|
connectCallback: ConnectCallback,
|
||||||
|
disconnectCallback: DisconnectCallback,
|
||||||
|
minDistance: number,
|
||||||
|
groupRadius: number,
|
||||||
|
onEnters: EntersCallback,
|
||||||
|
onMoves: MovesCallback,
|
||||||
|
onLeaves: LeavesCallback)
|
||||||
|
{
|
||||||
|
this.roomId = roomId;
|
||||||
|
this.anonymous = isRoomAnonymous(roomId);
|
||||||
|
this.tags = [];
|
||||||
|
this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY;
|
||||||
|
|
||||||
|
if (this.anonymous) {
|
||||||
|
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
||||||
|
} else {
|
||||||
|
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
|
||||||
|
this.roomSlug = roomSlug;
|
||||||
|
this.organizationSlug = organizationSlug;
|
||||||
|
this.worldSlug = worldSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.users = new Map<number, User>();
|
||||||
|
this.groups = new Set<Group>();
|
||||||
|
this.connectCallback = connectCallback;
|
||||||
|
this.disconnectCallback = disconnectCallback;
|
||||||
|
this.minDistance = minDistance;
|
||||||
|
this.groupRadius = groupRadius;
|
||||||
|
// A zone is 10 sprites wide.
|
||||||
|
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGroups(): Group[] {
|
||||||
|
return Array.from(this.groups.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUsers(): Map<number, User> {
|
||||||
|
return this.users;
|
||||||
|
}
|
||||||
|
|
||||||
|
public join(socket : ExSocketInterface, userPosition: PointInterface): void {
|
||||||
|
const user = new User(socket.userId, socket.userUuid, userPosition, false, this.positionNotifier, socket);
|
||||||
|
this.users.set(socket.userId, user);
|
||||||
|
// Let's call update position to trigger the join / leave room
|
||||||
|
//this.updatePosition(socket, userPosition);
|
||||||
|
this.updateUserGroup(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public leave(user : Identificable){
|
||||||
|
const userObj = this.users.get(user.userId);
|
||||||
|
if (userObj === undefined) {
|
||||||
|
console.warn('User ', user.userId, 'does not belong to world! It should!');
|
||||||
|
}
|
||||||
|
if (userObj !== undefined && typeof userObj.group !== 'undefined') {
|
||||||
|
this.leaveGroup(userObj);
|
||||||
|
}
|
||||||
|
this.users.delete(user.userId);
|
||||||
|
|
||||||
|
if (userObj !== undefined) {
|
||||||
|
this.positionNotifier.removeViewport(userObj);
|
||||||
|
this.positionNotifier.leave(userObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isFull(): boolean {
|
||||||
|
return this.users.size >= MAX_USERS_PER_ROOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return this.users.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePosition(socket : Identificable, userPosition: PointInterface): void {
|
||||||
|
const user = this.users.get(socket.userId);
|
||||||
|
if(typeof user === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setPosition(userPosition);
|
||||||
|
|
||||||
|
this.updateUserGroup(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateUserGroup(user: User): void {
|
||||||
|
user.group?.updatePosition();
|
||||||
|
|
||||||
|
if (user.silent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.group === undefined) {
|
||||||
|
// If the user is not part of a group:
|
||||||
|
// should he join a group?
|
||||||
|
|
||||||
|
// If the user is moving, don't try to join
|
||||||
|
if (user.getPosition().moving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
this.groups.add(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If the user is part of a group:
|
||||||
|
// should he leave the group?
|
||||||
|
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition());
|
||||||
|
if (distance > this.groupRadius) {
|
||||||
|
this.leaveGroup(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSilent(socket: Identificable, silent: boolean) {
|
||||||
|
const user = this.users.get(socket.userId);
|
||||||
|
if(typeof user === 'undefined') {
|
||||||
|
console.warn('In setSilent, could not find user with ID "'+socket.userId+'" in world.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.silent === silent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.silent = silent;
|
||||||
|
if (silent && user.group !== undefined) {
|
||||||
|
this.leaveGroup(user);
|
||||||
|
}
|
||||||
|
if (!silent) {
|
||||||
|
// If we are back to life, let's trigger a position update to see if we can join some group.
|
||||||
|
this.updatePosition(socket, user.getPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
*/
|
||||||
|
private leaveGroup(user: User): void {
|
||||||
|
const group = user.group;
|
||||||
|
if (group === undefined) {
|
||||||
|
throw new Error("The user is part of no group");
|
||||||
|
}
|
||||||
|
group.leave(user);
|
||||||
|
if (group.isEmpty()) {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
this.groups.delete(group);
|
||||||
|
//todo: is the group garbage collected?
|
||||||
|
} else {
|
||||||
|
group.updatePosition();
|
||||||
|
//this.positionNotifier.updatePosition(group, group.getPosition(), oldPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks for the closest user that is:
|
||||||
|
* - close enough (distance <= minDistance)
|
||||||
|
* - not in a group
|
||||||
|
* - not silent
|
||||||
|
* OR
|
||||||
|
* - close enough to a group (distance <= groupRadius)
|
||||||
|
*/
|
||||||
|
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') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(currentUser === user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentUser.silent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
||||||
|
|
||||||
|
if(distance <= minimumDistanceFound && distance <= this.minDistance) {
|
||||||
|
minimumDistanceFound = distance;
|
||||||
|
matchingItem = currentUser;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.groups.forEach((group: Group) => {
|
||||||
|
if (group.isFull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
||||||
|
if(distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
||||||
|
minimumDistanceFound = distance;
|
||||||
|
matchingItem = group;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchingItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
public setItemState(itemId: number, state: unknown) {
|
||||||
|
this.itemsState.set(itemId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItemsState(): Map<number, unknown> {
|
||||||
|
return this.itemsState;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setViewport(socket : Identificable, viewport: ViewportInterface): Movable[] {
|
||||||
|
const user = this.users.get(socket.userId);
|
||||||
|
if(typeof user === 'undefined') {
|
||||||
|
console.warn('In setViewport, could not find user with ID "'+socket.userId+'" in world.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.positionNotifier.setViewport(user, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
canAccess(userTags: string[]): boolean {
|
||||||
|
return arrayIntersect(userTags, this.tags);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,33 +1,49 @@
|
||||||
import { World, ConnectCallback, DisconnectCallback } from "./World";
|
import { ConnectCallback, DisconnectCallback } from "./GameRoom";
|
||||||
import { UserInterface } from "./UserInterface";
|
import { User } from "./User";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
import {uuid} from "uuidv4";
|
import {Movable} from "_Model/Movable";
|
||||||
|
import {PositionNotifier} from "_Model/PositionNotifier";
|
||||||
|
import {gaugeManager} from "../Services/GaugeManager";
|
||||||
|
|
||||||
export class Group {
|
export class Group implements Movable {
|
||||||
static readonly MAX_PER_GROUP = 4;
|
static readonly MAX_PER_GROUP = 4;
|
||||||
|
|
||||||
private id: string;
|
private static nextId: number = 1;
|
||||||
private users: UserInterface[];
|
|
||||||
private connectCallback: ConnectCallback;
|
private id: number;
|
||||||
private disconnectCallback: DisconnectCallback;
|
private users: Set<User>;
|
||||||
|
private x!: number;
|
||||||
|
private y!: number;
|
||||||
|
private hasEditedGauge: boolean = false;
|
||||||
|
private wasDestroyed: boolean = false;
|
||||||
|
private roomId: string;
|
||||||
|
|
||||||
|
|
||||||
constructor(users: UserInterface[], connectCallback: ConnectCallback, disconnectCallback: DisconnectCallback) {
|
constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) {
|
||||||
this.users = [];
|
this.roomId = roomId;
|
||||||
this.connectCallback = connectCallback;
|
this.users = new Set<User>();
|
||||||
this.disconnectCallback = disconnectCallback;
|
this.id = Group.nextId;
|
||||||
this.id = uuid();
|
Group.nextId++;
|
||||||
|
//we only send a event for prometheus metrics if the group lives more than 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.wasDestroyed) {
|
||||||
|
this.hasEditedGauge = true;
|
||||||
|
gaugeManager.incNbGroupsPerRoomGauge(roomId);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
users.forEach((user: UserInterface) => {
|
users.forEach((user: User) => {
|
||||||
this.join(user);
|
this.join(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.updatePosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsers(): UserInterface[] {
|
getUsers(): User[] {
|
||||||
return this.users;
|
return Array.from(this.users.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() : string{
|
getId() : number {
|
||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,65 +51,72 @@ export class Group {
|
||||||
* Returns the barycenter of all users (i.e. the center of the group)
|
* Returns the barycenter of all users (i.e. the center of the group)
|
||||||
*/
|
*/
|
||||||
getPosition(): PositionInterface {
|
getPosition(): PositionInterface {
|
||||||
let x = 0;
|
|
||||||
let y = 0;
|
|
||||||
// Let's compute the barycenter of all users.
|
|
||||||
this.users.forEach((user: UserInterface) => {
|
|
||||||
x += user.position.x;
|
|
||||||
y += user.position.y;
|
|
||||||
});
|
|
||||||
x /= this.users.length;
|
|
||||||
y /= this.users.length;
|
|
||||||
return {
|
return {
|
||||||
x,
|
x: this.x,
|
||||||
y
|
y: this.y
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the barycenter of all users (i.e. the center of the group)
|
||||||
|
*/
|
||||||
|
updatePosition(): void {
|
||||||
|
const oldX = this.x;
|
||||||
|
const oldY = this.y;
|
||||||
|
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
// Let's compute the barycenter of all users.
|
||||||
|
this.users.forEach((user: User) => {
|
||||||
|
const position = user.getPosition();
|
||||||
|
x += position.x;
|
||||||
|
y += position.y;
|
||||||
|
});
|
||||||
|
x /= this.users.size;
|
||||||
|
y /= this.users.size;
|
||||||
|
if (this.users.size === 0) {
|
||||||
|
throw new Error("EMPTY GROUP FOUND!!!");
|
||||||
|
}
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
|
||||||
|
if (oldX === undefined) {
|
||||||
|
this.positionNotifier.enter(this);
|
||||||
|
} else {
|
||||||
|
this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isFull(): boolean {
|
isFull(): boolean {
|
||||||
return this.users.length >= Group.MAX_PER_GROUP;
|
return this.users.size >= Group.MAX_PER_GROUP;
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
return this.users.length <= 1;
|
return this.users.size <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
join(user: UserInterface): void
|
join(user: User): void
|
||||||
{
|
{
|
||||||
// Broadcast on the right event
|
// Broadcast on the right event
|
||||||
this.connectCallback(user.id, this);
|
this.connectCallback(user, this);
|
||||||
this.users.push(user);
|
this.users.add(user);
|
||||||
user.group = this;
|
user.group = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
isPartOfGroup(user: UserInterface): boolean
|
leave(user: User): void
|
||||||
{
|
{
|
||||||
return this.users.includes(user);
|
const success = this.users.delete(user);
|
||||||
}
|
if (success === false) {
|
||||||
|
throw new Error("Could not find user "+user.id+" in the group "+this.id);
|
||||||
/*removeFromGroup(users: UserInterface[]): void
|
|
||||||
{
|
|
||||||
for(let i = 0; i < users.length; i++){
|
|
||||||
let user = users[i];
|
|
||||||
const index = this.users.indexOf(user, 0);
|
|
||||||
if (index > -1) {
|
|
||||||
this.users.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}*/
|
|
||||||
|
|
||||||
leave(user: UserInterface): void
|
|
||||||
{
|
|
||||||
const index = this.users.indexOf(user, 0);
|
|
||||||
if (index === -1) {
|
|
||||||
throw new Error("Could not find user in the group");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.users.splice(index, 1);
|
|
||||||
user.group = undefined;
|
user.group = undefined;
|
||||||
|
|
||||||
|
if (this.users.size !== 0) {
|
||||||
|
this.updatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
// Broadcast on the right event
|
// Broadcast on the right event
|
||||||
this.disconnectCallback(user.id, this);
|
this.disconnectCallback(user, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -102,8 +125,14 @@ export class Group {
|
||||||
*/
|
*/
|
||||||
destroy(): void
|
destroy(): void
|
||||||
{
|
{
|
||||||
this.users.forEach((user: UserInterface) => {
|
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
|
||||||
|
for (const user of this.users) {
|
||||||
this.leave(user);
|
this.leave(user);
|
||||||
})
|
}
|
||||||
|
this.wasDestroyed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get getSize(){
|
||||||
|
return this.users.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8
back/src/Model/Movable.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A physical object that can be placed into a Zone
|
||||||
|
*/
|
||||||
|
export interface Movable {
|
||||||
|
getPosition(): PositionInterface
|
||||||
|
}
|
132
back/src/Model/PositionNotifier.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
/**
|
||||||
|
* Tracks the position of every player on the map, and sends notifications to the players interested in knowing about the move
|
||||||
|
* (i.e. players that are looking at the zone the player is currently in)
|
||||||
|
*
|
||||||
|
* Internally, the PositionNotifier works with Zones. A zone is a square area of a map.
|
||||||
|
* Each player is in a given zone, and each player tracks one or many zones (depending on the player viewport)
|
||||||
|
*
|
||||||
|
* 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 {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
|
||||||
|
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||||
|
import {User} from "_Model/User";
|
||||||
|
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||||
|
import {Movable} from "_Model/Movable";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
|
||||||
|
interface ZoneDescriptor {
|
||||||
|
i: number;
|
||||||
|
j: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||||
|
return {
|
||||||
|
i: Math.floor(x / this.zoneWidth),
|
||||||
|
j: Math.floor(y / this.zoneHeight),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the viewport coordinates.
|
||||||
|
* Returns the list of new users to add
|
||||||
|
*/
|
||||||
|
public setViewport(user: User, viewport: ViewportInterface): Movable[] {
|
||||||
|
if (viewport.left > viewport.right || viewport.top > viewport.bottom) {
|
||||||
|
console.warn('Invalid viewport received: ', viewport);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldZones = user.listenedZones;
|
||||||
|
const newZones = new Set<Zone>();
|
||||||
|
|
||||||
|
const topLeftDesc = this.getZoneDescriptorFromCoordinates(viewport.left, viewport.top);
|
||||||
|
const bottomRightDesc = this.getZoneDescriptorFromCoordinates(viewport.right, viewport.bottom);
|
||||||
|
|
||||||
|
for (let j = topLeftDesc.j; j <= bottomRightDesc.j; j++) {
|
||||||
|
for (let i = topLeftDesc.i; i <= bottomRightDesc.i; i++) {
|
||||||
|
newZones.add(this.getZone(i, j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedZones = [...newZones].filter(x => !oldZones.has(x));
|
||||||
|
const removedZones = [...oldZones].filter(x => !newZones.has(x));
|
||||||
|
|
||||||
|
|
||||||
|
let things: Movable[] = [];
|
||||||
|
for (const zone of addedZones) {
|
||||||
|
zone.startListening(user);
|
||||||
|
things = things.concat(Array.from(zone.getThings()))
|
||||||
|
}
|
||||||
|
for (const zone of removedZones) {
|
||||||
|
zone.stopListening(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return things;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enter(thing: Movable): void {
|
||||||
|
const position = thing.getPosition();
|
||||||
|
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
||||||
|
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
||||||
|
zone.enter(thing, null, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): void {
|
||||||
|
// Did we change zone?
|
||||||
|
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
||||||
|
const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y);
|
||||||
|
|
||||||
|
if (oldZoneDesc.i != newZoneDesc.i || oldZoneDesc.j != newZoneDesc.j) {
|
||||||
|
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||||
|
const newZone = this.getZone(newZoneDesc.i, newZoneDesc.j);
|
||||||
|
|
||||||
|
// Leave old zone
|
||||||
|
oldZone.leave(thing, newZone);
|
||||||
|
|
||||||
|
// Enter new zone
|
||||||
|
newZone.enter(thing, oldZone, newPosition);
|
||||||
|
} else {
|
||||||
|
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||||
|
zone.move(thing, newPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public leave(thing: Movable): void {
|
||||||
|
const oldPosition = thing.getPosition();
|
||||||
|
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
||||||
|
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||||
|
oldZone.leave(thing, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeViewport(user: User): void {
|
||||||
|
// Also, let's stop listening on viewports
|
||||||
|
for (const zone of user.listenedZones) {
|
||||||
|
zone.stopListening(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getZone(i: number, j: number): Zone {
|
||||||
|
let zoneRow = this.zones[j];
|
||||||
|
if (zoneRow === undefined) {
|
||||||
|
zoneRow = new Array<Zone>();
|
||||||
|
this.zones[j] = zoneRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zone = this.zones[j][i];
|
||||||
|
if (zone === undefined) {
|
||||||
|
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, i, j);
|
||||||
|
this.zones[j][i] = zone;
|
||||||
|
}
|
||||||
|
return zone;
|
||||||
|
}
|
||||||
|
}
|
30
back/src/Model/RoomIdentifier.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
//helper functions to parse room IDs
|
||||||
|
|
||||||
|
export const isRoomAnonymous = (roomID: string): boolean => {
|
||||||
|
if (roomID.startsWith('_/')) {
|
||||||
|
return true;
|
||||||
|
} else if(roomID.startsWith('@/')) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw new Error('Incorrect room ID: '+roomID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
||||||
|
const idParts = roomId.split('/');
|
||||||
|
if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId);
|
||||||
|
return idParts.slice(2).join('/');
|
||||||
|
}
|
||||||
|
export interface extractDataFromPrivateRoomIdResponse {
|
||||||
|
organizationSlug: string;
|
||||||
|
worldSlug: string;
|
||||||
|
roomSlug: string;
|
||||||
|
}
|
||||||
|
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
||||||
|
const idParts = roomId.split('/');
|
||||||
|
if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId);
|
||||||
|
const organizationSlug = idParts[1];
|
||||||
|
const worldSlug = idParts[2];
|
||||||
|
const roomSlug = idParts[3];
|
||||||
|
return {organizationSlug, worldSlug, roomSlug}
|
||||||
|
}
|
35
back/src/Model/User.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { Group } from "./Group";
|
||||||
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
|
import {Zone} from "_Model/Zone";
|
||||||
|
import {Movable} from "_Model/Movable";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
import {PositionNotifier} from "_Model/PositionNotifier";
|
||||||
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
|
|
||||||
|
export class User implements Movable {
|
||||||
|
public listenedZones: Set<Zone>;
|
||||||
|
public group?: Group;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public id: number,
|
||||||
|
public uuid: string,
|
||||||
|
private position: PointInterface,
|
||||||
|
public silent: boolean,
|
||||||
|
private positionNotifier: PositionNotifier,
|
||||||
|
public readonly socket: ExSocketInterface
|
||||||
|
) {
|
||||||
|
this.listenedZones = new Set<Zone>();
|
||||||
|
|
||||||
|
this.positionNotifier.enter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPosition(): PointInterface {
|
||||||
|
return this.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPosition(position: PointInterface): void {
|
||||||
|
const oldPosition = this.position;
|
||||||
|
this.position = position;
|
||||||
|
this.positionNotifier.updatePosition(this, position, oldPosition);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
import { Group } from "./Group";
|
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
|
||||||
|
|
||||||
export interface UserInterface {
|
|
||||||
id: string,
|
|
||||||
group?: Group,
|
|
||||||
position: PointInterface
|
|
||||||
}
|
|
|
@ -1,14 +1,32 @@
|
||||||
import {Socket} from "socket.io";
|
|
||||||
import {PointInterface} from "./PointInterface";
|
import {PointInterface} from "./PointInterface";
|
||||||
import {Identificable} from "./Identificable";
|
import {Identificable} from "./Identificable";
|
||||||
import {TokenInterface} from "../../Controller/AuthenticateController";
|
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||||
|
import {BatchMessage, SubMessage} from "../../Messages/generated/messages_pb";
|
||||||
|
import {WebSocket} from "uWebSockets.js"
|
||||||
|
import {CharacterTexture} from "../../Services/AdminApi";
|
||||||
|
|
||||||
export interface ExSocketInterface extends Socket, Identificable {
|
export interface CharacterLayer {
|
||||||
|
name: string,
|
||||||
|
url: string|undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExSocketInterface extends WebSocket, Identificable {
|
||||||
token: string;
|
token: string;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
webRtcRoomId: string;
|
//userId: number; // A temporary (autoincremented) identifier for this user
|
||||||
userId: string;
|
userUuid: string; // A unique identifier for this user
|
||||||
name: string;
|
name: string;
|
||||||
character: string;
|
characterLayers: CharacterLayer[];
|
||||||
position: PointInterface;
|
position: PointInterface;
|
||||||
|
viewport: ViewportInterface;
|
||||||
|
/**
|
||||||
|
* Pushes an event that will be sent in the next batch of events
|
||||||
|
*/
|
||||||
|
emitInBatch: (payload: SubMessage) => void;
|
||||||
|
batchedMessages: BatchMessage;
|
||||||
|
batchTimeout: NodeJS.Timeout|null;
|
||||||
|
pingTimeout: NodeJS.Timeout|null;
|
||||||
|
disconnecting: boolean,
|
||||||
|
tags: string[],
|
||||||
|
textures: CharacterTexture[],
|
||||||
}
|
}
|
||||||
|
|
6
back/src/Model/Websocket/GroupUpdateInterface.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
|
||||||
|
export interface GroupUpdateInterface {
|
||||||
|
position: PositionInterface,
|
||||||
|
groupId: number,
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
export interface Identificable {
|
export interface Identificable {
|
||||||
userId: string;
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|
10
back/src/Model/Websocket/ItemEventMessage.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isItemEventMessageInterface =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
itemId: tg.isNumber,
|
||||||
|
event: tg.isString,
|
||||||
|
state: tg.isUnknown,
|
||||||
|
parameters: tg.isUnknown,
|
||||||
|
}).get();
|
||||||
|
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
|
@ -1,9 +1,11 @@
|
||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
import {isPointInterface} from "./PointInterface";
|
import {isPointInterface} from "./PointInterface";
|
||||||
|
import {isViewport} from "./ViewportMessage";
|
||||||
|
|
||||||
export const isJoinRoomMessageInterface =
|
export const isJoinRoomMessageInterface =
|
||||||
new tg.IsInterface().withProperties({
|
new tg.IsInterface().withProperties({
|
||||||
roomId: tg.isString,
|
roomId: tg.isString,
|
||||||
position: isPointInterface,
|
position: isPointInterface,
|
||||||
|
viewport: isViewport
|
||||||
}).get();
|
}).get();
|
||||||
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {PointInterface} from "_Model/Websocket/PointInterface";
|
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||||
|
|
||||||
export class MessageUserJoined {
|
export class MessageUserJoined {
|
||||||
constructor(public userId: string, public name: string, public character: string, public position: PointInterface) {
|
constructor(public userId: number, public name: string, public characterLayers: string[], public position: PointInterface) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import {PointInterface} from "./PointInterface";
|
|
||||||
|
|
||||||
export class MessageUserMoved {
|
|
||||||
constructor(public userId: string, public position: PointInterface) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,6 +6,6 @@ export class Point implements PointInterface{
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MessageUserPosition {
|
export class MessageUserPosition {
|
||||||
constructor(public userId: string, public name: string, public character: string, public position: PointInterface) {
|
constructor(public userId: number, public name: string, public characterLayers: string[], public position: PointInterface) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
108
back/src/Model/Websocket/ProtobufUtils.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import {PointInterface} from "./PointInterface";
|
||||||
|
import {
|
||||||
|
CharacterLayerMessage,
|
||||||
|
ItemEventMessage,
|
||||||
|
PointMessage,
|
||||||
|
PositionMessage
|
||||||
|
} from "../../Messages/generated/messages_pb";
|
||||||
|
import {CharacterLayer, ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
|
import Direction = PositionMessage.Direction;
|
||||||
|
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
|
||||||
|
export class ProtobufUtils {
|
||||||
|
|
||||||
|
public static toPositionMessage(point: PointInterface): PositionMessage {
|
||||||
|
let direction: PositionMessage.DirectionMap[keyof PositionMessage.DirectionMap];
|
||||||
|
switch (point.direction) {
|
||||||
|
case 'up':
|
||||||
|
direction = Direction.UP;
|
||||||
|
break;
|
||||||
|
case 'down':
|
||||||
|
direction = Direction.DOWN;
|
||||||
|
break;
|
||||||
|
case 'left':
|
||||||
|
direction = Direction.LEFT;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
direction = Direction.RIGHT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('unexpected direction');
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = new PositionMessage();
|
||||||
|
position.setX(point.x);
|
||||||
|
position.setY(point.y);
|
||||||
|
position.setMoving(point.moving);
|
||||||
|
position.setDirection(direction);
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toPointInterface(position: PositionMessage): PointInterface {
|
||||||
|
let direction: string;
|
||||||
|
switch (position.getDirection()) {
|
||||||
|
case Direction.UP:
|
||||||
|
direction = 'up';
|
||||||
|
break;
|
||||||
|
case Direction.DOWN:
|
||||||
|
direction = 'down';
|
||||||
|
break;
|
||||||
|
case Direction.LEFT:
|
||||||
|
direction = 'left';
|
||||||
|
break;
|
||||||
|
case Direction.RIGHT:
|
||||||
|
direction = 'right';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unexpected direction");
|
||||||
|
}
|
||||||
|
|
||||||
|
// sending to all clients in room except sender
|
||||||
|
return {
|
||||||
|
x: position.getX(),
|
||||||
|
y: position.getY(),
|
||||||
|
direction,
|
||||||
|
moving: position.getMoving(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toPointMessage(point: PositionInterface): PointMessage {
|
||||||
|
const position = new PointMessage();
|
||||||
|
position.setX(Math.floor(point.x));
|
||||||
|
position.setY(Math.floor(point.y));
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toItemEvent(itemEventMessage: ItemEventMessage): ItemEventMessageInterface {
|
||||||
|
return {
|
||||||
|
itemId: itemEventMessage.getItemid(),
|
||||||
|
event: itemEventMessage.getEvent(),
|
||||||
|
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
||||||
|
state: JSON.parse(itemEventMessage.getStatejson()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
||||||
|
const itemEventMessage = new ItemEventMessage();
|
||||||
|
itemEventMessage.setItemid(itemEvent.itemId);
|
||||||
|
itemEventMessage.setEvent(itemEvent.event);
|
||||||
|
itemEventMessage.setParametersjson(JSON.stringify(itemEvent.parameters));
|
||||||
|
itemEventMessage.setStatejson(JSON.stringify(itemEvent.state));
|
||||||
|
|
||||||
|
return itemEventMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
||||||
|
return characterLayers.map(function(characterLayer): CharacterLayerMessage {
|
||||||
|
const message = new CharacterLayerMessage();
|
||||||
|
message.setName(characterLayer.name);
|
||||||
|
if (characterLayer.url) {
|
||||||
|
message.setUrl(characterLayer.url);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,6 @@ import * as tg from "generic-type-guard";
|
||||||
export const isSetPlayerDetailsMessage =
|
export const isSetPlayerDetailsMessage =
|
||||||
new tg.IsInterface().withProperties({
|
new tg.IsInterface().withProperties({
|
||||||
name: tg.isString,
|
name: tg.isString,
|
||||||
character: tg.isString
|
characterLayers: tg.isArray(tg.isString)
|
||||||
}).get();
|
}).get();
|
||||||
export type SetPlayerDetailsMessage = tg.GuardedType<typeof isSetPlayerDetailsMessage>;
|
export type SetPlayerDetailsMessage = tg.GuardedType<typeof isSetPlayerDetailsMessage>;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export interface UserInGroupInterface {
|
export interface UserInGroupInterface {
|
||||||
userId: string,
|
userId: number,
|
||||||
name: string,
|
name: string,
|
||||||
initiator: boolean
|
initiator: boolean
|
||||||
}
|
}
|
||||||
|
|
10
back/src/Model/Websocket/ViewportMessage.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isViewport =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
left: tg.isNumber,
|
||||||
|
top: tg.isNumber,
|
||||||
|
right: tg.isNumber,
|
||||||
|
bottom: tg.isNumber,
|
||||||
|
}).get();
|
||||||
|
export type ViewportInterface = tg.GuardedType<typeof isViewport>;
|
|
@ -1,10 +1,18 @@
|
||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isSignalData =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
type: tg.isOptional(tg.isString)
|
||||||
|
}).get();
|
||||||
|
|
||||||
export const isWebRtcSignalMessageInterface =
|
export const isWebRtcSignalMessageInterface =
|
||||||
new tg.IsInterface().withProperties({
|
new tg.IsInterface().withProperties({
|
||||||
userId: tg.isString,
|
receiverId: tg.isNumber,
|
||||||
receiverId: tg.isString,
|
signal: isSignalData
|
||||||
roomId: tg.isString,
|
}).get();
|
||||||
signal: tg.isUnknown
|
export const isWebRtcScreenSharingStartMessageInterface =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
userId: tg.isNumber,
|
||||||
|
roomId: tg.isString
|
||||||
}).get();
|
}).get();
|
||||||
export type WebRtcSignalMessageInterface = tg.GuardedType<typeof isWebRtcSignalMessageInterface>;
|
export type WebRtcSignalMessageInterface = tg.GuardedType<typeof isWebRtcSignalMessageInterface>;
|
||||||
|
|
|
@ -1,287 +0,0 @@
|
||||||
import {MessageUserPosition, Point} from "./Websocket/MessageUserPosition";
|
|
||||||
import {PointInterface} from "./Websocket/PointInterface";
|
|
||||||
import {Group} from "./Group";
|
|
||||||
import {Distance} from "./Distance";
|
|
||||||
import {UserInterface} from "./UserInterface";
|
|
||||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
|
||||||
import {Identificable} from "_Model/Websocket/Identificable";
|
|
||||||
|
|
||||||
export type ConnectCallback = (user: string, group: Group) => void;
|
|
||||||
export type DisconnectCallback = (user: string, group: Group) => void;
|
|
||||||
|
|
||||||
// callback called when a group is created or moved or changes users
|
|
||||||
export type GroupUpdatedCallback = (group: Group) => void;
|
|
||||||
export type GroupDeletedCallback = (uuid: string, lastUser: UserInterface) => void;
|
|
||||||
|
|
||||||
export class World {
|
|
||||||
private readonly minDistance: number;
|
|
||||||
private readonly groupRadius: number;
|
|
||||||
|
|
||||||
// Users, sorted by ID
|
|
||||||
private readonly users: Map<string, UserInterface>;
|
|
||||||
private readonly groups: Group[];
|
|
||||||
|
|
||||||
private readonly connectCallback: ConnectCallback;
|
|
||||||
private readonly disconnectCallback: DisconnectCallback;
|
|
||||||
private readonly groupUpdatedCallback: GroupUpdatedCallback;
|
|
||||||
private readonly groupDeletedCallback: GroupDeletedCallback;
|
|
||||||
|
|
||||||
constructor(connectCallback: ConnectCallback,
|
|
||||||
disconnectCallback: DisconnectCallback,
|
|
||||||
minDistance: number,
|
|
||||||
groupRadius: number,
|
|
||||||
groupUpdatedCallback: GroupUpdatedCallback,
|
|
||||||
groupDeletedCallback: GroupDeletedCallback)
|
|
||||||
{
|
|
||||||
this.users = new Map<string, UserInterface>();
|
|
||||||
this.groups = [];
|
|
||||||
this.connectCallback = connectCallback;
|
|
||||||
this.disconnectCallback = disconnectCallback;
|
|
||||||
this.minDistance = minDistance;
|
|
||||||
this.groupRadius = groupRadius;
|
|
||||||
this.groupUpdatedCallback = groupUpdatedCallback;
|
|
||||||
this.groupDeletedCallback = groupDeletedCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getGroups(): Group[] {
|
|
||||||
return this.groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getUsers(): Map<string, UserInterface> {
|
|
||||||
return this.users;
|
|
||||||
}
|
|
||||||
|
|
||||||
public join(socket : Identificable, userPosition: PointInterface): void {
|
|
||||||
this.users.set(socket.userId, {
|
|
||||||
id: socket.userId,
|
|
||||||
position: userPosition
|
|
||||||
});
|
|
||||||
// Let's call update position to trigger the join / leave room
|
|
||||||
this.updatePosition(socket, userPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
public leave(user : Identificable){
|
|
||||||
const userObj = this.users.get(user.userId);
|
|
||||||
if (userObj === undefined) {
|
|
||||||
console.warn('User ', user.userId, 'does not belong to world! It should!');
|
|
||||||
}
|
|
||||||
if (userObj !== undefined && typeof userObj.group !== 'undefined') {
|
|
||||||
this.leaveGroup(userObj);
|
|
||||||
}
|
|
||||||
this.users.delete(user.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public updatePosition(socket : Identificable, userPosition: PointInterface): void {
|
|
||||||
const user = this.users.get(socket.userId);
|
|
||||||
if(typeof user === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.position = userPosition;
|
|
||||||
|
|
||||||
if (typeof user.group === 'undefined') {
|
|
||||||
// If the user is not part of a group:
|
|
||||||
// should he join a group?
|
|
||||||
const closestItem: UserInterface|Group|null = this.searchClosestAvailableUserOrGroup(user);
|
|
||||||
|
|
||||||
if (closestItem !== null) {
|
|
||||||
if (closestItem instanceof Group) {
|
|
||||||
// Let's join the group!
|
|
||||||
closestItem.join(user);
|
|
||||||
} else {
|
|
||||||
const closestUser : UserInterface = closestItem;
|
|
||||||
const group: Group = new Group([
|
|
||||||
user,
|
|
||||||
closestUser
|
|
||||||
], this.connectCallback, this.disconnectCallback);
|
|
||||||
this.groups.push(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// If the user is part of a group:
|
|
||||||
// should he leave the group?
|
|
||||||
const distance = World.computeDistanceBetweenPositions(user.position, user.group.getPosition());
|
|
||||||
if (distance > this.groupRadius) {
|
|
||||||
this.leaveGroup(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At the very end, if the user is part of a group, let's call the callback to update group position
|
|
||||||
if (typeof user.group !== 'undefined') {
|
|
||||||
this.groupUpdatedCallback(user.group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
|
|
||||||
*
|
|
||||||
* @param user
|
|
||||||
*/
|
|
||||||
private leaveGroup(user: UserInterface): void {
|
|
||||||
const group = user.group;
|
|
||||||
if (typeof group === 'undefined') {
|
|
||||||
throw new Error("The user is part of no group");
|
|
||||||
}
|
|
||||||
group.leave(user);
|
|
||||||
if (group.isEmpty()) {
|
|
||||||
this.groupDeletedCallback(group.getId(), user);
|
|
||||||
group.destroy();
|
|
||||||
const index = this.groups.indexOf(group, 0);
|
|
||||||
if (index === -1) {
|
|
||||||
throw new Error("Could not find group");
|
|
||||||
}
|
|
||||||
this.groups.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
this.groupUpdatedCallback(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks for the closest user that is:
|
|
||||||
* - close enough (distance <= minDistance)
|
|
||||||
* - not in a group
|
|
||||||
* OR
|
|
||||||
* - close enough to a group (distance <= groupRadius)
|
|
||||||
*/
|
|
||||||
private searchClosestAvailableUserOrGroup(user: UserInterface): UserInterface|Group|null
|
|
||||||
{
|
|
||||||
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
|
||||||
let matchingItem: UserInterface | 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') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(currentUser === user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const distance = World.computeDistance(user, currentUser); // compute distance between peers.
|
|
||||||
|
|
||||||
if(distance <= minimumDistanceFound && distance <= this.minDistance) {
|
|
||||||
minimumDistanceFound = distance;
|
|
||||||
matchingItem = currentUser;
|
|
||||||
}
|
|
||||||
/*if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) {
|
|
||||||
// We found a user we can bind to.
|
|
||||||
return;
|
|
||||||
}*/
|
|
||||||
/*
|
|
||||||
if(context.groups.length > 0) {
|
|
||||||
|
|
||||||
context.groups.forEach(group => {
|
|
||||||
if(group.isPartOfGroup(userPosition)) { // Is the user in a group ?
|
|
||||||
if(group.isStillIn(userPosition)) { // Is the user leaving the group ? (is the user at more than max distance of each player)
|
|
||||||
|
|
||||||
// Should we split the group? (is each player reachable from the current player?)
|
|
||||||
// This is needed if
|
|
||||||
// A <==> B <==> C <===> D
|
|
||||||
// becomes A <==> B <=====> C <> D
|
|
||||||
// If C moves right, the distance between B and C is too great and we must form 2 groups
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the user is in no group
|
|
||||||
// Is there someone in a group close enough and with room in the group ?
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Aucun groupe n'existe donc je stock les users assez proches de moi
|
|
||||||
let dist: Distance = {
|
|
||||||
distance: distance,
|
|
||||||
first: userPosition,
|
|
||||||
second: user // TODO: convertir en messageUserPosition
|
|
||||||
}
|
|
||||||
usersToBeGroupedWith.push(dist);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
|
|
||||||
this.groups.forEach((group: Group) => {
|
|
||||||
if (group.isFull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const distance = World.computeDistanceBetweenPositions(user.position, group.getPosition());
|
|
||||||
if(distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
|
||||||
minimumDistanceFound = distance;
|
|
||||||
matchingItem = group;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return matchingItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static computeDistance(user1: UserInterface, user2: UserInterface): number
|
|
||||||
{
|
|
||||||
return Math.sqrt(Math.pow(user2.position.x - user1.position.x, 2) + Math.pow(user2.position.y - user1.position.y, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*getDistancesBetweenGroupUsers(group: Group): Distance[]
|
|
||||||
{
|
|
||||||
let i = 0;
|
|
||||||
let users = group.getUsers();
|
|
||||||
let distances: Distance[] = [];
|
|
||||||
users.forEach(function(user1, key1) {
|
|
||||||
users.forEach(function(user2, key2) {
|
|
||||||
if(key1 < key2) {
|
|
||||||
distances[i] = {
|
|
||||||
distance: World.computeDistance(user1, user2),
|
|
||||||
first: user1,
|
|
||||||
second: user2
|
|
||||||
};
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
distances.sort(World.compareDistances);
|
|
||||||
|
|
||||||
return distances;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterGroup(distances: Distance[], group: Group): void
|
|
||||||
{
|
|
||||||
let users = group.getUsers();
|
|
||||||
let usersToRemove = false;
|
|
||||||
let groupTmp: MessageUserPosition[] = [];
|
|
||||||
distances.forEach(dist => {
|
|
||||||
if(dist.distance <= World.MIN_DISTANCE) {
|
|
||||||
let users = [dist.first];
|
|
||||||
let usersbis = [dist.second]
|
|
||||||
groupTmp.push(dist.first);
|
|
||||||
groupTmp.push(dist.second);
|
|
||||||
} else {
|
|
||||||
usersToRemove = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if(usersToRemove) {
|
|
||||||
// Detecte le ou les users qui se sont fait sortir du groupe
|
|
||||||
let difference = users.filter(x => !groupTmp.includes(x));
|
|
||||||
|
|
||||||
// TODO : Notify users un difference that they have left the group
|
|
||||||
}
|
|
||||||
|
|
||||||
let newgroup = new Group(groupTmp);
|
|
||||||
this.groups.push(newgroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static compareDistances(distA: Distance, distB: Distance): number
|
|
||||||
{
|
|
||||||
if (distA.distance < distB.distance) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (distA.distance > distB.distance) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}*/
|
|
||||||
}
|
|
109
back/src/Model/Zone.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import {User} from "./User";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
import {Movable} from "./Movable";
|
||||||
|
import {Group} from "./Group";
|
||||||
|
|
||||||
|
export type EntersCallback = (thing: Movable, listener: User) => void;
|
||||||
|
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: User) => void;
|
||||||
|
export type LeavesCallback = (thing: Movable, listener: User) => void;
|
||||||
|
|
||||||
|
export class Zone {
|
||||||
|
private things: Set<Movable> = new Set<Movable>();
|
||||||
|
private listeners: Set<User> = new Set<User>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param x For debugging purpose only
|
||||||
|
* @param y For debugging purpose only
|
||||||
|
*/
|
||||||
|
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private x: number, private y: number) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user/thing leaves the zone
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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+')');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
this.notifyLeft(thing, newZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify listeners of this zone that this user/thing left
|
||||||
|
*/
|
||||||
|
private notifyLeft(thing: Movable, newZone: Zone|null) {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
if (listener !== thing && (newZone === null || !listener.listenedZones.has(newZone))) {
|
||||||
|
this.onLeaves(thing, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
|
||||||
|
this.things.add(thing);
|
||||||
|
this.notifyEnter(thing, oldZone, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify listeners of this zone that this user entered
|
||||||
|
*/
|
||||||
|
private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
if (listener === thing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (oldZone === null || !listener.listenedZones.has(oldZone)) {
|
||||||
|
this.onEnters(thing, listener);
|
||||||
|
} else {
|
||||||
|
this.onMoves(thing, position, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public move(thing: Movable, position: PositionInterface) {
|
||||||
|
if (!this.things.has(thing)) {
|
||||||
|
this.things.add(thing);
|
||||||
|
this.notifyEnter(thing, null, position);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
if (listener !== thing) {
|
||||||
|
this.onMoves(thing,position, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public startListening(listener: User): void {
|
||||||
|
for (const thing of this.things) {
|
||||||
|
if (thing !== listener) {
|
||||||
|
this.onEnters(thing, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.add(listener);
|
||||||
|
listener.listenedZones.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopListening(listener: User): void {
|
||||||
|
for (const thing of this.things) {
|
||||||
|
if (thing !== listener) {
|
||||||
|
this.onLeaves(thing, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
listener.listenedZones.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getThings(): Set<Movable> {
|
||||||
|
return this.things;
|
||||||
|
}
|
||||||
|
}
|
13
back/src/Server/server/app.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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 = {}) {
|
||||||
|
super(options); // eslint-disable-line constructor-super
|
||||||
|
extend(this, new BaseApp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
116
back/src/Server/server/baseapp.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
res.bodyStream = function() {
|
||||||
|
const stream = new Readable();
|
||||||
|
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
||||||
|
|
||||||
|
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
||||||
|
// uint and then slicing is bit faster than slice and then uint
|
||||||
|
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
if (isLast) {
|
||||||
|
stream.push(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
class BaseApp {
|
||||||
|
_sockets = new Map();
|
||||||
|
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}.`);
|
||||||
|
this._post(pattern, (res, req) => {
|
||||||
|
handleBody(res, req);
|
||||||
|
handler(res, req);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
put(pattern: string, handler: Handler) {
|
||||||
|
if (typeof handler !== 'function')
|
||||||
|
throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
|
this._put(pattern, (res, req) => {
|
||||||
|
handleBody(res, req);
|
||||||
|
|
||||||
|
handler(res, req);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(pattern: string, handler: Handler) {
|
||||||
|
if (typeof handler !== 'function')
|
||||||
|
throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
|
this._patch(pattern, (res, req) => {
|
||||||
|
handleBody(res, req);
|
||||||
|
|
||||||
|
handler(res, req);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
cb(socket);
|
||||||
|
});
|
||||||
|
} else if (typeof h === 'number' && typeof p === 'function') {
|
||||||
|
this._listen(h, socket => {
|
||||||
|
this._sockets.set(h, socket);
|
||||||
|
p(socket);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error(
|
||||||
|
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(port: null | number = null) {
|
||||||
|
if (port) {
|
||||||
|
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
||||||
|
this._sockets.delete(port);
|
||||||
|
} else {
|
||||||
|
this._sockets.forEach(app => {
|
||||||
|
us_listen_socket_close(app);
|
||||||
|
});
|
||||||
|
this._sockets.clear();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseApp;
|
100
back/src/Server/server/formdata.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import Busboy from 'busboy';
|
||||||
|
import mkdirp from 'mkdirp';
|
||||||
|
|
||||||
|
function formData(
|
||||||
|
contType: string,
|
||||||
|
options: busboy.BusboyConfig & {
|
||||||
|
abortOnLimit?: boolean;
|
||||||
|
tmpDir?: string;
|
||||||
|
onFile?: (
|
||||||
|
fieldname: string,
|
||||||
|
file: NodeJS.ReadableStream,
|
||||||
|
filename: string,
|
||||||
|
encoding: string,
|
||||||
|
mimetype: string
|
||||||
|
) => string;
|
||||||
|
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
filename?: (oldName: string) => string;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
console.log('Enter form data');
|
||||||
|
options.headers = {
|
||||||
|
'content-type': contType
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const busb = new Busboy(options);
|
||||||
|
const ret = {};
|
||||||
|
|
||||||
|
this.bodyStream().pipe(busb);
|
||||||
|
|
||||||
|
busb.on('limit', () => {
|
||||||
|
if (options.abortOnLimit) {
|
||||||
|
reject(Error('limit'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
||||||
|
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = {
|
||||||
|
filename,
|
||||||
|
encoding,
|
||||||
|
mimetype,
|
||||||
|
filePath: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof options.tmpDir === 'string') {
|
||||||
|
if (typeof options.filename === 'function') filename = options.filename(filename);
|
||||||
|
const fileToSave = join(options.tmpDir, filename);
|
||||||
|
mkdirp(dirname(fileToSave));
|
||||||
|
|
||||||
|
file.pipe(createWriteStream(fileToSave));
|
||||||
|
value.filePath = fileToSave;
|
||||||
|
}
|
||||||
|
if (typeof options.onFile === 'function') {
|
||||||
|
value.filePath =
|
||||||
|
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRetValue(ret, fieldname, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on('field', function(fieldname, value) {
|
||||||
|
if (typeof options.onField === 'function') options.onField(fieldname, value);
|
||||||
|
|
||||||
|
setRetValue(ret, fieldname, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on('finish', function() {
|
||||||
|
resolve(ret);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRetValue(
|
||||||
|
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
fieldname: string,
|
||||||
|
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
) {
|
||||||
|
if (fieldname.endsWith('[]')) {
|
||||||
|
fieldname = fieldname.slice(0, fieldname.length - 2);
|
||||||
|
if (Array.isArray(ret[fieldname])) {
|
||||||
|
ret[fieldname].push(value);
|
||||||
|
} else {
|
||||||
|
ret[fieldname] = [value];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Array.isArray(ret[fieldname])) {
|
||||||
|
ret[fieldname].push(value);
|
||||||
|
} else if (ret[fieldname]) {
|
||||||
|
ret[fieldname] = [ret[fieldname], value];
|
||||||
|
} else {
|
||||||
|
ret[fieldname] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default formData;
|
13
back/src/Server/server/sslapp.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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) {
|
||||||
|
super(options); // eslint-disable-line constructor-super
|
||||||
|
extend(this, new BaseApp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SSLApp;
|
11
back/src/Server/server/types.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
||||||
|
|
||||||
|
export type UwsApp = {
|
||||||
|
(options: AppOptions): TemplatedApp;
|
||||||
|
new (options: AppOptions): TemplatedApp;
|
||||||
|
prototype: TemplatedApp;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
||||||
|
|
||||||
|
export {};
|
37
back/src/Server/server/utils.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
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;
|
||||||
|
if (who[prop] && overwrite) {
|
||||||
|
who[`_${prop}`] = who[prop];
|
||||||
|
}
|
||||||
|
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 => {
|
||||||
|
const buffers: Buffer[] = [];
|
||||||
|
stream.on('data', buffers.push.bind(buffers));
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
switch (buffers.length) {
|
||||||
|
case 0:
|
||||||
|
resolve(Buffer.allocUnsafe(0));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
resolve(buffers[0]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
resolve(Buffer.concat(buffers));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { extend, stob };
|
19
back/src/Server/sifrr.server.ts
Normal file
|
@ -0,0 +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';
|
||||||
|
|
||||||
|
const getQuery = (req: HttpRequest) => {
|
||||||
|
return parse(req.getQuery());
|
||||||
|
};
|
||||||
|
|
||||||
|
export { App, SSLApp, getQuery };
|
||||||
|
export * from './server/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
App,
|
||||||
|
SSLApp,
|
||||||
|
getQuery,
|
||||||
|
...types
|
||||||
|
};
|
115
back/src/Services/AdminApi.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
|
||||||
|
import Axios from "axios";
|
||||||
|
import {v4} from "uuid";
|
||||||
|
|
||||||
|
export interface AdminApiData {
|
||||||
|
organizationSlug: string
|
||||||
|
worldSlug: string
|
||||||
|
roomSlug: string
|
||||||
|
mapUrlStart: string
|
||||||
|
tags: string[]
|
||||||
|
policy_type: number
|
||||||
|
userUuid: string
|
||||||
|
messages?: unknown[],
|
||||||
|
textures: CharacterTexture[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacterTexture {
|
||||||
|
id: number,
|
||||||
|
level: number,
|
||||||
|
url: string,
|
||||||
|
rights: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchMemberDataByUuidResponse {
|
||||||
|
uuid: string;
|
||||||
|
tags: string[];
|
||||||
|
textures: CharacterTexture[];
|
||||||
|
messages: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdminApi {
|
||||||
|
|
||||||
|
async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise<AdminApiData> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject('No admin backoffice set!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
|
||||||
|
organizationSlug,
|
||||||
|
worldSlug
|
||||||
|
};
|
||||||
|
|
||||||
|
if (roomSlug) {
|
||||||
|
params.roomSlug = roomSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await Axios.get(ADMIN_API_URL + '/api/map',
|
||||||
|
{
|
||||||
|
headers: {"Authorization": `${ADMIN_API_TOKEN}`},
|
||||||
|
params
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMemberDataByUuid(uuid: string): Promise<FetchMemberDataByUuidResponse> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject('No admin backoffice set!');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid,
|
||||||
|
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||||
|
)
|
||||||
|
return res.data;
|
||||||
|
} catch (e) {
|
||||||
|
if (e?.response?.status == 404) {
|
||||||
|
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
|
||||||
|
console.warn('Cannot find user with uuid "'+uuid+'". Performing an anonymous login instead.');
|
||||||
|
return {
|
||||||
|
uuid: v4(),
|
||||||
|
tags: [],
|
||||||
|
textures: [],
|
||||||
|
messages: [],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject('No admin backoffice set!');
|
||||||
|
}
|
||||||
|
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
||||||
|
const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken,
|
||||||
|
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||||
|
)
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject('No admin backoffice set!');
|
||||||
|
}
|
||||||
|
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
||||||
|
const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken,
|
||||||
|
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||||
|
)
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) {
|
||||||
|
return Axios.post(`${ADMIN_API_URL}/api/report`, {
|
||||||
|
reportedUserUuid,
|
||||||
|
reportedUserComment,
|
||||||
|
reporterUserUuid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {"Authorization": `${ADMIN_API_TOKEN}`}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adminApi = new AdminApi();
|
3
back/src/Services/ArrayHelper.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const arrayIntersect = (array1: string[], array2: string[]) : boolean => {
|
||||||
|
return array1.filter(value => array2.includes(value)).length > 0;
|
||||||
|
}
|
32
back/src/Services/ClientEventsEmitter.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
const clientJoinEvent = 'clientJoin';
|
||||||
|
const clientLeaveEvent = 'clientLeave';
|
||||||
|
|
||||||
|
class ClientEventsEmitter extends EventEmitter {
|
||||||
|
emitClientJoin(clientUUid: string, roomId: string): void {
|
||||||
|
this.emit(clientJoinEvent, clientUUid, roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitClientLeave(clientUUid: string, roomId: string): void {
|
||||||
|
this.emit(clientLeaveEvent, clientUUid, roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void {
|
||||||
|
this.on(clientJoinEvent, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerToClientLeave(callback: (clientUUid: string, roomId: string) => void): void {
|
||||||
|
this.on(clientLeaveEvent, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterFromClientJoin(callback: (clientUUid: string, roomId: string) => void): void {
|
||||||
|
this.removeListener(clientJoinEvent, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterFromClientLeave(callback: (clientUUid: string, roomId: string) => void): void {
|
||||||
|
this.removeListener(clientLeaveEvent, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clientEventsEmitter = new ClientEventsEmitter();
|
55
back/src/Services/CpuTracker.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
function secNSec2ms(secNSec: Array<number>|number) {
|
||||||
|
if (Array.isArray(secNSec)) {
|
||||||
|
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
||||||
|
}
|
||||||
|
return secNSec / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CpuTracker {
|
||||||
|
private cpuPercent: number = 0;
|
||||||
|
private overHeating: boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
let time = process.hrtime.bigint()
|
||||||
|
let usage = process.cpuUsage()
|
||||||
|
setInterval(() => {
|
||||||
|
const elapTime = process.hrtime.bigint();
|
||||||
|
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)
|
||||||
|
|
||||||
|
time = elapTime;
|
||||||
|
|
||||||
|
if (!this.overHeating && this.cpuPercent > CPU_OVERHEAT_THRESHOLD) {
|
||||||
|
this.overHeating = true;
|
||||||
|
console.warn('CPU high threshold alert. Going in "overheat" mode');
|
||||||
|
} else if (this.overHeating && this.cpuPercent <= CPU_OVERHEAT_THRESHOLD) {
|
||||||
|
this.overHeating = false;
|
||||||
|
console.log('CPU is back to normal. Canceling "overheat" mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*console.log('elapsed time ms: ', elapTimeMS)
|
||||||
|
console.log('elapsed user ms: ', elapUserMS)
|
||||||
|
console.log('elapsed system ms:', elapSystMS)
|
||||||
|
console.log('cpu percent: ', this.cpuPercent)*/
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCpuPercent(): number {
|
||||||
|
return this.cpuPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isOverHeating(): boolean {
|
||||||
|
return this.overHeating;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuTracker = new CpuTracker();
|
||||||
|
|
||||||
|
export { cpuTracker };
|
54
back/src/Services/GaugeManager.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import {Counter, Gauge} from "prom-client";
|
||||||
|
|
||||||
|
//this class should manage all the custom metrics used by prometheus
|
||||||
|
class GaugeManager {
|
||||||
|
private nbClientsGauge: Gauge<string>;
|
||||||
|
private nbClientsPerRoomGauge: Gauge<string>;
|
||||||
|
private nbGroupsPerRoomGauge: Gauge<string>;
|
||||||
|
private nbGroupsPerRoomCounter: Counter<string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.nbClientsGauge = new Gauge({
|
||||||
|
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' ]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.nbGroupsPerRoomCounter = new Counter({
|
||||||
|
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' ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
incNbClientPerRoomGauge(roomId: string): void {
|
||||||
|
this.nbClientsGauge.inc();
|
||||||
|
this.nbClientsPerRoomGauge.inc({ room: roomId });
|
||||||
|
}
|
||||||
|
|
||||||
|
decNbClientPerRoomGauge(roomId: string): void {
|
||||||
|
this.nbClientsGauge.dec();
|
||||||
|
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
||||||
|
}
|
||||||
|
|
||||||
|
incNbGroupsPerRoomGauge(roomId: string): void {
|
||||||
|
this.nbGroupsPerRoomCounter.inc({ room: roomId })
|
||||||
|
this.nbGroupsPerRoomGauge.inc({ room: roomId })
|
||||||
|
}
|
||||||
|
|
||||||
|
decNbGroupsPerRoomGauge(roomId: string): void {
|
||||||
|
this.nbGroupsPerRoomGauge.dec({ room: roomId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gaugeManager = new GaugeManager();
|
50
back/src/Services/IoSocketHelpers.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
|
import {BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
|
export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void {
|
||||||
|
socket.batchedMessages.addPayload(payload);
|
||||||
|
|
||||||
|
if (socket.batchTimeout === null) {
|
||||||
|
socket.batchTimeout = setTimeout(() => {
|
||||||
|
if (socket.disconnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setBatchmessage(socket.batchedMessages);
|
||||||
|
|
||||||
|
socket.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
socket.batchedMessages = new BatchMessage();
|
||||||
|
socket.batchTimeout = null;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we send a message, we don't need to keep the connection alive
|
||||||
|
resetPing(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetPing(ws: ExSocketInterface): void {
|
||||||
|
if (ws.pingTimeout) {
|
||||||
|
clearTimeout(ws.pingTimeout);
|
||||||
|
}
|
||||||
|
ws.pingTimeout = setTimeout(() => {
|
||||||
|
if (ws.disconnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.ping();
|
||||||
|
resetPing(ws);
|
||||||
|
}, 29000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitError(Client: ExSocketInterface, message: string): void {
|
||||||
|
const errorMessage = new ErrorMessage();
|
||||||
|
errorMessage.setMessage(message);
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
|
if (!Client.disconnecting) {
|
||||||
|
Client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
console.warn(message);
|
||||||
|
}
|
72
back/src/Services/JWTTokenManager.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable";
|
||||||
|
import {uuid} from "uuidv4";
|
||||||
|
import Jwt from "jsonwebtoken";
|
||||||
|
import {TokenInterface} from "../Controller/AuthenticateController";
|
||||||
|
import {adminApi, AdminApiData} from "../Services/AdminApi";
|
||||||
|
|
||||||
|
class JWTTokenManager {
|
||||||
|
|
||||||
|
public createJWTToken(userUuid: string) {
|
||||||
|
return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUserUuidFromToken(token: unknown): Promise<string> {
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('An authentication error happened, a user tried to connect without a token.');
|
||||||
|
}
|
||||||
|
if (typeof(token) !== "string") {
|
||||||
|
throw new Error('Token is expected to be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(token === 'test') {
|
||||||
|
if (ALLOW_ARTILLERY) {
|
||||||
|
return uuid();
|
||||||
|
} else {
|
||||||
|
throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => {
|
||||||
|
const tokenInterface = tokenDecoded as TokenInterface;
|
||||||
|
if (err) {
|
||||||
|
console.error('An authentication error happened, invalid JsonWebToken.', err);
|
||||||
|
reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tokenDecoded === undefined) {
|
||||||
|
console.error('Empty token found.');
|
||||||
|
reject(new Error('Empty token found.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//verify token
|
||||||
|
if (!this.isValidToken(tokenInterface)) {
|
||||||
|
reject(new Error('Authentication error, invalid token structure.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//verify user in admin
|
||||||
|
adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => {
|
||||||
|
resolve(tokenInterface.userUuid);
|
||||||
|
}).catch((err) => {
|
||||||
|
//anonymous user
|
||||||
|
if(err.response && err.response.status && err.response.status === 404){
|
||||||
|
resolve(tokenInterface.userUuid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(new Error('Authentication error, invalid token structure. ' + err));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidToken(token: object): token is TokenInterface {
|
||||||
|
return !(typeof((token as TokenInterface).userUuid) !== 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jwtTokenManager = new JWTTokenManager();
|
706
back/src/Services/SocketManager.ts
Normal file
|
@ -0,0 +1,706 @@
|
||||||
|
import {GameRoom} from "../Model/GameRoom";
|
||||||
|
import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface";
|
||||||
|
import {
|
||||||
|
GroupDeleteMessage,
|
||||||
|
GroupUpdateMessage,
|
||||||
|
ItemEventMessage,
|
||||||
|
ItemStateMessage,
|
||||||
|
PlayGlobalMessage,
|
||||||
|
PointMessage,
|
||||||
|
PositionMessage,
|
||||||
|
RoomJoinedMessage,
|
||||||
|
ServerToClientMessage,
|
||||||
|
SetPlayerDetailsMessage,
|
||||||
|
SilentMessage,
|
||||||
|
SubMessage,
|
||||||
|
ReportPlayerMessage,
|
||||||
|
UserJoinedMessage, UserLeftMessage,
|
||||||
|
UserMovedMessage,
|
||||||
|
UserMovesMessage,
|
||||||
|
ViewportMessage, WebRtcDisconnectMessage,
|
||||||
|
WebRtcSignalToClientMessage,
|
||||||
|
WebRtcSignalToServerMessage,
|
||||||
|
WebRtcStartMessage,
|
||||||
|
QueryJitsiJwtMessage,
|
||||||
|
SendJitsiJwtMessage,
|
||||||
|
SendUserMessage
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
|
import {PointInterface} from "../Model/Websocket/PointInterface";
|
||||||
|
import {User} from "../Model/User";
|
||||||
|
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
|
||||||
|
import {Group} from "../Model/Group";
|
||||||
|
import {cpuTracker} from "./CpuTracker";
|
||||||
|
import {isSetPlayerDetailsMessage} from "../Model/Websocket/SetPlayerDetailsMessage";
|
||||||
|
import {GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable";
|
||||||
|
import {Movable} from "../Model/Movable";
|
||||||
|
import {PositionInterface} from "../Model/PositionInterface";
|
||||||
|
import {adminApi, CharacterTexture} from "./AdminApi";
|
||||||
|
import Direction = PositionMessage.Direction;
|
||||||
|
import {emitError, emitInBatch} from "./IoSocketHelpers";
|
||||||
|
import Jwt from "jsonwebtoken";
|
||||||
|
import {JITSI_URL} from "../Enum/EnvironmentVariable";
|
||||||
|
import {clientEventsEmitter} from "./ClientEventsEmitter";
|
||||||
|
import {gaugeManager} from "./GaugeManager";
|
||||||
|
|
||||||
|
interface AdminSocketRoomsList {
|
||||||
|
[index: string]: number;
|
||||||
|
}
|
||||||
|
interface AdminSocketUsersList {
|
||||||
|
[index: string]: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminSocketData {
|
||||||
|
rooms: AdminSocketRoomsList,
|
||||||
|
users: AdminSocketUsersList,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SocketManager {
|
||||||
|
private Worlds: Map<string, GameRoom> = new Map<string, GameRoom>();
|
||||||
|
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
||||||
|
gaugeManager.incNbClientPerRoomGauge(roomId);
|
||||||
|
});
|
||||||
|
clientEventsEmitter.registerToClientLeave((clientUUid: string, roomId: string) => {
|
||||||
|
gaugeManager.decNbClientPerRoomGauge(roomId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAdminSocketDataFor(roomId:string): AdminSocketData {
|
||||||
|
const data:AdminSocketData = {
|
||||||
|
rooms: {},
|
||||||
|
users: {},
|
||||||
|
}
|
||||||
|
const room = this.Worlds.get(roomId);
|
||||||
|
if (room === undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const users = room.getUsers();
|
||||||
|
data.rooms[roomId] = users.size;
|
||||||
|
users.forEach(user => {
|
||||||
|
data.users[user.uuid] = true
|
||||||
|
})
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleJoinRoom(client: ExSocketInterface): void {
|
||||||
|
const position = client.position;
|
||||||
|
const viewport = client.viewport;
|
||||||
|
try {
|
||||||
|
this.sockets.set(client.userId, client); //todo: should this be at the end of the function?
|
||||||
|
//join new previous room
|
||||||
|
const gameRoom = this.joinRoom(client, position);
|
||||||
|
|
||||||
|
const things = gameRoom.setViewport(client, viewport);
|
||||||
|
|
||||||
|
const roomJoinedMessage = new RoomJoinedMessage();
|
||||||
|
|
||||||
|
for (const thing of things) {
|
||||||
|
if (thing instanceof User) {
|
||||||
|
const player: ExSocketInterface|undefined = this.sockets.get(thing.id);
|
||||||
|
if (player === undefined) {
|
||||||
|
console.warn('Something went wrong. The World contains a user "'+thing.id+"' but this user does not exist in the sockets list!");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userJoinedMessage = new UserJoinedMessage();
|
||||||
|
userJoinedMessage.setUserid(thing.id);
|
||||||
|
userJoinedMessage.setName(player.name);
|
||||||
|
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(player.characterLayers));
|
||||||
|
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(player.position));
|
||||||
|
|
||||||
|
roomJoinedMessage.addUser(userJoinedMessage);
|
||||||
|
roomJoinedMessage.setTagList(client.tags);
|
||||||
|
} else if (thing instanceof Group) {
|
||||||
|
const groupUpdateMessage = new GroupUpdateMessage();
|
||||||
|
groupUpdateMessage.setGroupid(thing.getId());
|
||||||
|
groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition()));
|
||||||
|
|
||||||
|
roomJoinedMessage.addGroup(groupUpdateMessage);
|
||||||
|
} else {
|
||||||
|
console.error("Unexpected type for Movable returned by setViewport");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [itemId, item] of gameRoom.getItemsState().entries()) {
|
||||||
|
const itemStateMessage = new ItemStateMessage();
|
||||||
|
itemStateMessage.setItemid(itemId);
|
||||||
|
itemStateMessage.setStatejson(JSON.stringify(item));
|
||||||
|
|
||||||
|
roomJoinedMessage.addItem(itemStateMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
roomJoinedMessage.setCurrentuserid(client.userId);
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage);
|
||||||
|
|
||||||
|
if (!client.disconnecting) {
|
||||||
|
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "join_room" event');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleViewport(client: ExSocketInterface, viewportMessage: ViewportMessage) {
|
||||||
|
try {
|
||||||
|
const viewport = viewportMessage.toObject();
|
||||||
|
|
||||||
|
client.viewport = viewport;
|
||||||
|
|
||||||
|
const world = this.Worlds.get(client.roomId);
|
||||||
|
if (!world) {
|
||||||
|
console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
world.setViewport(client, client.viewport);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "SET_VIEWPORT" event');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUserMovesMessage(client: ExSocketInterface, userMovesMessage: UserMovesMessage) {
|
||||||
|
try {
|
||||||
|
const userMoves = userMovesMessage.toObject();
|
||||||
|
|
||||||
|
// If CPU is high, let's drop messages of users moving (we will only dispatch the final position)
|
||||||
|
if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = userMoves.position;
|
||||||
|
if (position === undefined) {
|
||||||
|
throw new Error('Position not found in message');
|
||||||
|
}
|
||||||
|
const viewport = userMoves.viewport;
|
||||||
|
if (viewport === undefined) {
|
||||||
|
throw new Error('Viewport not found in message');
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction: string;
|
||||||
|
switch (position.direction) {
|
||||||
|
case Direction.UP:
|
||||||
|
direction = 'up';
|
||||||
|
break;
|
||||||
|
case Direction.DOWN:
|
||||||
|
direction = 'down';
|
||||||
|
break;
|
||||||
|
case Direction.LEFT:
|
||||||
|
direction = 'left';
|
||||||
|
break;
|
||||||
|
case Direction.RIGHT:
|
||||||
|
direction = 'right';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unexpected direction");
|
||||||
|
}
|
||||||
|
|
||||||
|
// sending to all clients in room except sender
|
||||||
|
client.position = {
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
direction,
|
||||||
|
moving: position.moving,
|
||||||
|
};
|
||||||
|
client.viewport = viewport;
|
||||||
|
|
||||||
|
// update position in the world
|
||||||
|
const world = this.Worlds.get(client.roomId);
|
||||||
|
if (!world) {
|
||||||
|
console.error("In USER_POSITION, could not find world with id '", client.roomId, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
world.updatePosition(client, client.position);
|
||||||
|
world.setViewport(client, client.viewport);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "user_position" event');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Useless now, will be useful again if we allow editing details in game
|
||||||
|
handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||||
|
const playerDetails = {
|
||||||
|
name: playerDetailsMessage.getName(),
|
||||||
|
characterLayers: playerDetailsMessage.getCharacterlayersList()
|
||||||
|
};
|
||||||
|
//console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails);
|
||||||
|
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||||
|
emitError(client, 'Invalid SET_PLAYER_DETAILS message received: ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.name = playerDetails.name;
|
||||||
|
client.characterLayers = SocketManager.mergeCharacterLayersAndCustomTextures(playerDetails.characterLayers, client.textures);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) {
|
||||||
|
try {
|
||||||
|
// update position in the world
|
||||||
|
const world = this.Worlds.get(client.roomId);
|
||||||
|
if (!world) {
|
||||||
|
console.error("In handleSilentMessage, could not find world with id '", client.roomId, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
world.setSilent(client, silentMessage.getSilent());
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "handleSilentMessage"');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleItemEvent(ws: ExSocketInterface, itemEventMessage: ItemEventMessage) {
|
||||||
|
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const world = this.Worlds.get(ws.roomId);
|
||||||
|
if (!world) {
|
||||||
|
console.error("Could not find world with id '", ws.roomId, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
subMessage.setItemeventmessage(itemEventMessage);
|
||||||
|
|
||||||
|
// Let's send the event without using the SocketIO room.
|
||||||
|
for (const user of world.getUsers().values()) {
|
||||||
|
const client = this.searchClientByIdOrFail(user.id);
|
||||||
|
//client.emit(SocketIoEvent.ITEM_EVENT, itemEvent);
|
||||||
|
emitInBatch(client, subMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
world.setItemState(itemEvent.itemId, itemEvent.state);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "item_event"');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) {
|
||||||
|
try {
|
||||||
|
const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid());
|
||||||
|
if (!reportedSocket) {
|
||||||
|
throw 'reported socket user not found';
|
||||||
|
}
|
||||||
|
//TODO report user on admin application
|
||||||
|
await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "handleReportMessage"');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitVideo(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void {
|
||||||
|
//send only at user
|
||||||
|
const client = this.sockets.get(data.getReceiverid());
|
||||||
|
if (client === undefined) {
|
||||||
|
console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
|
||||||
|
webrtcSignalToClient.setUserid(socket.userId);
|
||||||
|
webrtcSignalToClient.setSignal(data.getSignal());
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
|
||||||
|
|
||||||
|
if (!client.disconnecting) {
|
||||||
|
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitScreenSharing(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void {
|
||||||
|
//send only at user
|
||||||
|
const client = this.sockets.get(data.getReceiverid());
|
||||||
|
if (client === undefined) {
|
||||||
|
console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
|
||||||
|
webrtcSignalToClient.setUserid(socket.userId);
|
||||||
|
webrtcSignalToClient.setSignal(data.getSignal());
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
|
||||||
|
|
||||||
|
if (!client.disconnecting) {
|
||||||
|
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private searchClientByIdOrFail(userId: number): ExSocketInterface {
|
||||||
|
const client: ExSocketInterface|undefined = this.sockets.get(userId);
|
||||||
|
if (client === undefined) {
|
||||||
|
throw new Error("Could not find user with id " + userId);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveRoom(Client : ExSocketInterface){
|
||||||
|
// leave previous room and world
|
||||||
|
if(Client.roomId){
|
||||||
|
try {
|
||||||
|
//user leave previous world
|
||||||
|
const world: GameRoom | undefined = this.Worlds.get(Client.roomId);
|
||||||
|
if (world) {
|
||||||
|
world.leave(Client);
|
||||||
|
if (world.isEmpty()) {
|
||||||
|
this.Worlds.delete(Client.roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//user leave previous room
|
||||||
|
//Client.leave(Client.roomId);
|
||||||
|
} finally {
|
||||||
|
//delete Client.roomId;
|
||||||
|
this.sockets.delete(Client.userId);
|
||||||
|
clientEventsEmitter.emitClientLeave(Client.userUuid, Client.roomId);
|
||||||
|
console.log('A user left (', this.sockets.size, ' connected users)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
||||||
|
//check and create new world for a room
|
||||||
|
let world = this.Worlds.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, listener: User) => this.onRoomEnter(thing, listener),
|
||||||
|
(thing: Movable, position:PositionInterface, listener:User) => this.onClientMove(thing, position, listener),
|
||||||
|
(thing: Movable, listener:User) => this.onClientLeave(thing, listener)
|
||||||
|
);
|
||||||
|
if (!world.anonymous) {
|
||||||
|
const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug)
|
||||||
|
world.tags = data.tags
|
||||||
|
world.policyType = Number(data.policy_type)
|
||||||
|
}
|
||||||
|
this.Worlds.set(roomId, world);
|
||||||
|
}
|
||||||
|
return Promise.resolve(world)
|
||||||
|
}
|
||||||
|
|
||||||
|
private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom {
|
||||||
|
|
||||||
|
const roomId = client.roomId;
|
||||||
|
client.position = position;
|
||||||
|
|
||||||
|
const world = this.Worlds.get(roomId)
|
||||||
|
if(world === undefined){
|
||||||
|
throw new Error('Could not find room for ID: '+client.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch groups position to newly connected user
|
||||||
|
world.getGroups().forEach((group: Group) => {
|
||||||
|
this.emitCreateUpdateGroupEvent(client, group);
|
||||||
|
});
|
||||||
|
//join world
|
||||||
|
world.join(client, client.position);
|
||||||
|
clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId);
|
||||||
|
console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)');
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRoomEnter(thing: Movable, listener: User) {
|
||||||
|
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||||
|
if (thing instanceof User) {
|
||||||
|
const clientUser = this.searchClientByIdOrFail(thing.id);
|
||||||
|
|
||||||
|
const userJoinedMessage = new UserJoinedMessage();
|
||||||
|
if (!Number.isInteger(clientUser.userId)) {
|
||||||
|
throw new Error('clientUser.userId is not an integer '+clientUser.userId);
|
||||||
|
}
|
||||||
|
userJoinedMessage.setUserid(clientUser.userId);
|
||||||
|
userJoinedMessage.setName(clientUser.name);
|
||||||
|
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(clientUser.characterLayers));
|
||||||
|
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position));
|
||||||
|
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
subMessage.setUserjoinedmessage(userJoinedMessage);
|
||||||
|
|
||||||
|
emitInBatch(clientListener, subMessage);
|
||||||
|
} else if (thing instanceof Group) {
|
||||||
|
this.emitCreateUpdateGroupEvent(clientListener, thing);
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected type for Movable.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClientMove(thing: Movable, position:PositionInterface, listener:User): void {
|
||||||
|
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||||
|
if (thing instanceof User) {
|
||||||
|
const clientUser = this.searchClientByIdOrFail(thing.id);
|
||||||
|
|
||||||
|
const userMovedMessage = new UserMovedMessage();
|
||||||
|
userMovedMessage.setUserid(clientUser.userId);
|
||||||
|
userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position));
|
||||||
|
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
subMessage.setUsermovedmessage(userMovedMessage);
|
||||||
|
|
||||||
|
clientListener.emitInBatch(subMessage);
|
||||||
|
//console.log("Sending USER_MOVED event");
|
||||||
|
} else if (thing instanceof Group) {
|
||||||
|
this.emitCreateUpdateGroupEvent(clientListener, thing);
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected type for Movable.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClientLeave(thing: Movable, listener:User) {
|
||||||
|
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||||
|
if (thing instanceof User) {
|
||||||
|
const clientUser = this.searchClientByIdOrFail(thing.id);
|
||||||
|
this.emitUserLeftEvent(clientListener, clientUser.userId);
|
||||||
|
} else if (thing instanceof Group) {
|
||||||
|
this.emitDeleteGroupEvent(clientListener, thing.getId());
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected type for Movable.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitCreateUpdateGroupEvent(client: ExSocketInterface, group: Group): void {
|
||||||
|
const position = group.getPosition();
|
||||||
|
const pointMessage = new PointMessage();
|
||||||
|
pointMessage.setX(Math.floor(position.x));
|
||||||
|
pointMessage.setY(Math.floor(position.y));
|
||||||
|
const groupUpdateMessage = new GroupUpdateMessage();
|
||||||
|
groupUpdateMessage.setGroupid(group.getId());
|
||||||
|
groupUpdateMessage.setPosition(pointMessage);
|
||||||
|
groupUpdateMessage.setGroupsize(group.getSize);
|
||||||
|
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
subMessage.setGroupupdatemessage(groupUpdateMessage);
|
||||||
|
|
||||||
|
emitInBatch(client, subMessage);
|
||||||
|
//socket.emit(SocketIoEvent.GROUP_CREATE_UPDATE, groupUpdateMessage.serializeBinary().buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitDeleteGroupEvent(client: ExSocketInterface, groupId: number): void {
|
||||||
|
const groupDeleteMessage = new GroupDeleteMessage();
|
||||||
|
groupDeleteMessage.setGroupid(groupId);
|
||||||
|
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
subMessage.setGroupdeletemessage(groupDeleteMessage);
|
||||||
|
|
||||||
|
emitInBatch(client, subMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitUserLeftEvent(client: ExSocketInterface, userId: number): void {
|
||||||
|
const userLeftMessage = new UserLeftMessage();
|
||||||
|
userLeftMessage.setUserid(userId);
|
||||||
|
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
subMessage.setUserleftmessage(userLeftMessage);
|
||||||
|
|
||||||
|
emitInBatch(client, subMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private joinWebRtcRoom(user: User, group: Group) {
|
||||||
|
/*const roomId: string = "webrtcroom"+group.getId();
|
||||||
|
if (user.socket.webRtcRoomId === roomId) {
|
||||||
|
return;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
for (const otherUser of group.getUsers()) {
|
||||||
|
if (user === otherUser) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's send 2 messages: one to the user joining the group and one to the other user
|
||||||
|
const webrtcStartMessage1 = new WebRtcStartMessage();
|
||||||
|
webrtcStartMessage1.setUserid(otherUser.id);
|
||||||
|
webrtcStartMessage1.setName(otherUser.socket.name);
|
||||||
|
webrtcStartMessage1.setInitiator(true);
|
||||||
|
|
||||||
|
const serverToClientMessage1 = new ServerToClientMessage();
|
||||||
|
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||||
|
|
||||||
|
if (!user.socket.disconnecting) {
|
||||||
|
user.socket.send(serverToClientMessage1.serializeBinary().buffer, true);
|
||||||
|
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const webrtcStartMessage2 = new WebRtcStartMessage();
|
||||||
|
webrtcStartMessage2.setUserid(user.id);
|
||||||
|
webrtcStartMessage2.setName(user.socket.name);
|
||||||
|
webrtcStartMessage2.setInitiator(false);
|
||||||
|
|
||||||
|
const serverToClientMessage2 = new ServerToClientMessage();
|
||||||
|
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
||||||
|
|
||||||
|
if (!otherUser.socket.disconnecting) {
|
||||||
|
otherUser.socket.send(serverToClientMessage2.serializeBinary().buffer, true);
|
||||||
|
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//disconnect user
|
||||||
|
private disConnectedUser(user: User, group: Group) {
|
||||||
|
// Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection
|
||||||
|
// which will be shut for the other player).
|
||||||
|
// However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player,
|
||||||
|
// the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing).
|
||||||
|
// So we also send the disconnect event to the other player.
|
||||||
|
for (const otherUser of group.getUsers()) {
|
||||||
|
if (user === otherUser) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webrtcDisconnectMessage1 = new WebRtcDisconnectMessage();
|
||||||
|
webrtcDisconnectMessage1.setUserid(user.id);
|
||||||
|
|
||||||
|
const serverToClientMessage1 = new ServerToClientMessage();
|
||||||
|
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
|
||||||
|
|
||||||
|
if (!otherUser.socket.disconnecting) {
|
||||||
|
otherUser.socket.send(serverToClientMessage1.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
|
||||||
|
webrtcDisconnectMessage2.setUserid(otherUser.id);
|
||||||
|
|
||||||
|
const serverToClientMessage2 = new ServerToClientMessage();
|
||||||
|
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
|
||||||
|
|
||||||
|
if (!user.socket.disconnecting) {
|
||||||
|
user.socket.send(serverToClientMessage2.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
|
||||||
|
try {
|
||||||
|
const world = this.Worlds.get(client.roomId);
|
||||||
|
if (!world) {
|
||||||
|
console.error("In emitPlayGlobalMessage, could not find world with id '", client.roomId, "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setPlayglobalmessage(playglobalmessage);
|
||||||
|
|
||||||
|
for (const [id, user] of world.getUsers().entries()) {
|
||||||
|
user.socket.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('An error occurred on "emitPlayGlobalMessage" event');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public getWorlds(): Map<string, GameRoom> {
|
||||||
|
return this.Worlds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
searchClientByUuid(uuid: string): ExSocketInterface | null {
|
||||||
|
for(const socket of this.sockets.values()){
|
||||||
|
if(socket.userUuid === uuid){
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public handleQueryJitsiJwtMessage(client: ExSocketInterface, 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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's see if the current client has
|
||||||
|
const isAdmin = client.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:
|
||||||
|
{
|
||||||
|
"alg": "HS256",
|
||||||
|
"typ": "JWT"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendJitsiJwtMessage = new SendJitsiJwtMessage();
|
||||||
|
sendJitsiJwtMessage.setJitsiroom(room);
|
||||||
|
sendJitsiJwtMessage.setJwt(jwt);
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setSendjitsijwtmessage(sendJitsiJwtMessage);
|
||||||
|
|
||||||
|
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public emitSendUserMessage(messageToSend: {userUuid: string, message: string, type: string}): ExSocketInterface {
|
||||||
|
const socket = this.searchClientByUuid(messageToSend.userUuid);
|
||||||
|
if(!socket){
|
||||||
|
throw 'socket was not found';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendUserMessage = new SendUserMessage();
|
||||||
|
sendUserMessage.setMessage(messageToSend.message);
|
||||||
|
sendUserMessage.setType(messageToSend.type);
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setSendusermessage(sendUserMessage);
|
||||||
|
|
||||||
|
if (!socket.disconnecting) {
|
||||||
|
socket.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the characterLayers received from the front (as an array of string) with the custom textures from the back.
|
||||||
|
*/
|
||||||
|
static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] {
|
||||||
|
const characterLayerObjs: CharacterLayer[] = [];
|
||||||
|
for (const characterLayer of characterLayers) {
|
||||||
|
if (characterLayer.startsWith('customCharacterTexture')) {
|
||||||
|
const customCharacterLayerId: number = +characterLayer.substr(22);
|
||||||
|
for (const memberTexture of memberTextures) {
|
||||||
|
if (memberTexture.id == customCharacterLayerId) {
|
||||||
|
characterLayerObjs.push({
|
||||||
|
name: characterLayer,
|
||||||
|
url: memberTexture.url
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
characterLayerObjs.push({
|
||||||
|
name: characterLayer,
|
||||||
|
url: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return characterLayerObjs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const socketManager = new SocketManager();
|
14
back/tests/ArrayHelperTest.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import {arrayIntersect} from "../src/Services/ArrayHelper";
|
||||||
|
|
||||||
|
|
||||||
|
describe("RoomIdentifier", () => {
|
||||||
|
it("should return true on intersect", () => {
|
||||||
|
expect(arrayIntersect(['admin', 'user'], ['admin', 'superAdmin'])).toBe(true);
|
||||||
|
});
|
||||||
|
it("should be reflexive", () => {
|
||||||
|
expect(arrayIntersect(['admin', 'superAdmin'], ['admin', 'user'])).toBe(true);
|
||||||
|
});
|
||||||
|
it("should return false on non intersect", () => {
|
||||||
|
expect(arrayIntersect(['admin', 'user'], ['superAdmin'])).toBe(false);
|
||||||
|
});
|
||||||
|
})
|
97
back/tests/GameRoomTest.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import "jasmine";
|
||||||
|
import {GameRoom, ConnectCallback, DisconnectCallback } from "../src/Model/GameRoom";
|
||||||
|
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||||
|
import { Group } from "../src/Model/Group";
|
||||||
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
|
import {User} from "_Model/User";
|
||||||
|
|
||||||
|
function createMockUser(userId: number): ExSocketInterface {
|
||||||
|
return {
|
||||||
|
userId
|
||||||
|
} as ExSocketInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("GameRoom", () => {
|
||||||
|
it("should connect user1 and user2", () => {
|
||||||
|
let connectCalledNumber: number = 0;
|
||||||
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
|
connectCalledNumber++;
|
||||||
|
}
|
||||||
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||||
|
|
||||||
|
world.join(createMockUser(1), new Point(100, 100));
|
||||||
|
|
||||||
|
world.join(createMockUser(2), new Point(500, 100));
|
||||||
|
|
||||||
|
world.updatePosition({ userId: 2 }, new Point(261, 100));
|
||||||
|
|
||||||
|
expect(connectCalledNumber).toBe(0);
|
||||||
|
|
||||||
|
world.updatePosition({ userId: 2 }, new Point(101, 100));
|
||||||
|
|
||||||
|
expect(connectCalledNumber).toBe(2);
|
||||||
|
|
||||||
|
world.updatePosition({ userId: 2 }, new Point(102, 100));
|
||||||
|
expect(connectCalledNumber).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should connect 3 users", () => {
|
||||||
|
let connectCalled: boolean = false;
|
||||||
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
|
connectCalled = true;
|
||||||
|
}
|
||||||
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||||
|
|
||||||
|
world.join(createMockUser(1), new Point(100, 100));
|
||||||
|
|
||||||
|
world.join(createMockUser(2), new Point(200, 100));
|
||||||
|
|
||||||
|
expect(connectCalled).toBe(true);
|
||||||
|
connectCalled = false;
|
||||||
|
|
||||||
|
// baz joins at the outer limit of the group
|
||||||
|
world.join(createMockUser(3), new Point(311, 100));
|
||||||
|
|
||||||
|
expect(connectCalled).toBe(false);
|
||||||
|
|
||||||
|
world.updatePosition({ userId: 3 }, new Point(309, 100));
|
||||||
|
|
||||||
|
expect(connectCalled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disconnect user1 and user2", () => {
|
||||||
|
let connectCalled: boolean = false;
|
||||||
|
let disconnectCallNumber: number = 0;
|
||||||
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
|
connectCalled = true;
|
||||||
|
}
|
||||||
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||||
|
disconnectCallNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||||
|
|
||||||
|
world.join(createMockUser(1), new Point(100, 100));
|
||||||
|
|
||||||
|
world.join(createMockUser(2), new Point(259, 100));
|
||||||
|
|
||||||
|
expect(connectCalled).toBe(true);
|
||||||
|
expect(disconnectCallNumber).toBe(0);
|
||||||
|
|
||||||
|
world.updatePosition({ userId: 2 }, new Point(100+160+160+1, 100));
|
||||||
|
|
||||||
|
expect(disconnectCallNumber).toBe(2);
|
||||||
|
|
||||||
|
world.updatePosition({ userId: 2 }, new Point(262, 100));
|
||||||
|
expect(disconnectCallNumber).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
176
back/tests/PositionNotifierTest.ts
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import "jasmine";
|
||||||
|
import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom";
|
||||||
|
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||||
|
import { Group } from "../src/Model/Group";
|
||||||
|
import {PositionNotifier} from "../src/Model/PositionNotifier";
|
||||||
|
import {User} from "../src/Model/User";
|
||||||
|
import {PointInterface} from "../src/Model/Websocket/PointInterface";
|
||||||
|
import {Zone} from "_Model/Zone";
|
||||||
|
import {Movable} from "_Model/Movable";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
|
|
||||||
|
|
||||||
|
describe("PositionNotifier", () => {
|
||||||
|
it("should receive notifications when player moves", () => {
|
||||||
|
let enterTriggered = false;
|
||||||
|
let moveTriggered = false;
|
||||||
|
let leaveTriggered = false;
|
||||||
|
|
||||||
|
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => {
|
||||||
|
enterTriggered = true;
|
||||||
|
}, (thing: Movable, position: PositionInterface) => {
|
||||||
|
moveTriggered = true;
|
||||||
|
}, (thing: Movable) => {
|
||||||
|
leaveTriggered = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = new User(1, 'test', {
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
}, false, positionNotifier, {} as ExSocketInterface);
|
||||||
|
|
||||||
|
const user2 = new User(2, 'test', {
|
||||||
|
x: -9999,
|
||||||
|
y: -9999,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
}, false, positionNotifier, {} as ExSocketInterface);
|
||||||
|
|
||||||
|
positionNotifier.setViewport(user1, {
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(true);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
enterTriggered = false;
|
||||||
|
|
||||||
|
// Move inside the zone
|
||||||
|
user2.setPosition({x:501, y:500, direction: 'down', moving: false});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(true);
|
||||||
|
moveTriggered = false;
|
||||||
|
|
||||||
|
// Move out of the zone in a zone that we don't track
|
||||||
|
user2.setPosition({x: 901, y: 500, direction: 'down', moving: false});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(true);
|
||||||
|
leaveTriggered = false;
|
||||||
|
|
||||||
|
// Move back in
|
||||||
|
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||||
|
expect(enterTriggered).toBe(true);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(false);
|
||||||
|
enterTriggered = false;
|
||||||
|
|
||||||
|
// Move out of the zone in a zone that we do track
|
||||||
|
user2.setPosition({x: 200, y: 500, direction: 'down', moving: false});
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(true);
|
||||||
|
expect(leaveTriggered).toBe(false);
|
||||||
|
moveTriggered = false;
|
||||||
|
|
||||||
|
// Leave the room
|
||||||
|
positionNotifier.leave(user2);
|
||||||
|
positionNotifier.removeViewport(user2);
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(true);
|
||||||
|
leaveTriggered = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should receive notifications when camera moves", () => {
|
||||||
|
let enterTriggered = false;
|
||||||
|
let moveTriggered = false;
|
||||||
|
let leaveTriggered = false;
|
||||||
|
|
||||||
|
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => {
|
||||||
|
enterTriggered = true;
|
||||||
|
}, (thing: Movable, position: PositionInterface) => {
|
||||||
|
moveTriggered = true;
|
||||||
|
}, (thing: Movable) => {
|
||||||
|
leaveTriggered = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = new User(1, 'test', {
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
}, false, positionNotifier, {} as ExSocketInterface);
|
||||||
|
|
||||||
|
const user2 = new User(2, 'test', {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
}, false, positionNotifier, {} as ExSocketInterface);
|
||||||
|
|
||||||
|
let newUsers = positionNotifier.setViewport(user1, {
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newUsers.length).toBe(2);
|
||||||
|
expect(enterTriggered).toBe(true);
|
||||||
|
enterTriggered = false;
|
||||||
|
|
||||||
|
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(true);
|
||||||
|
moveTriggered = false;
|
||||||
|
|
||||||
|
// Move the viewport but the user stays inside.
|
||||||
|
positionNotifier.setViewport(user1, {
|
||||||
|
left: 201,
|
||||||
|
right: 601,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(false);
|
||||||
|
|
||||||
|
// Move the viewport out of the user.
|
||||||
|
positionNotifier.setViewport(user1, {
|
||||||
|
left: 901,
|
||||||
|
right: 1001,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(false);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(true);
|
||||||
|
leaveTriggered = false;
|
||||||
|
|
||||||
|
// Move the viewport back on the user.
|
||||||
|
newUsers = positionNotifier.setViewport(user1, {
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
bottom: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(enterTriggered).toBe(true);
|
||||||
|
expect(moveTriggered).toBe(false);
|
||||||
|
expect(leaveTriggered).toBe(false);
|
||||||
|
enterTriggered = false;
|
||||||
|
expect(newUsers.length).toBe(2);
|
||||||
|
});
|
||||||
|
})
|
19
back/tests/RoomIdentifierTest.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
|
||||||
|
|
||||||
|
describe("RoomIdentifier", () => {
|
||||||
|
it("should flag public id as anonymous", () => {
|
||||||
|
expect(isRoomAnonymous('_/global/test')).toBe(true);
|
||||||
|
});
|
||||||
|
it("should flag public id as not anonymous", () => {
|
||||||
|
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
|
||||||
|
});
|
||||||
|
it("should extract roomSlug from public ID", () => {
|
||||||
|
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
|
||||||
|
});
|
||||||
|
it("should extract correct from private ID", () => {
|
||||||
|
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
|
||||||
|
expect(organizationSlug).toBe('afup');
|
||||||
|
expect(worldSlug).toBe('afup2020');
|
||||||
|
expect(roomSlug).toBe('1floor');
|
||||||
|
});
|
||||||
|
})
|
|
@ -1,89 +0,0 @@
|
||||||
import "jasmine";
|
|
||||||
import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World";
|
|
||||||
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
|
||||||
import { Group } from "../src/Model/Group";
|
|
||||||
|
|
||||||
describe("World", () => {
|
|
||||||
it("should connect user1 and user2", () => {
|
|
||||||
let connectCalledNumber: number = 0;
|
|
||||||
const connect: ConnectCallback = (user: string, group: Group): void => {
|
|
||||||
connectCalledNumber++;
|
|
||||||
}
|
|
||||||
const disconnect: DisconnectCallback = (user: string, group: Group): void => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
|
||||||
|
|
||||||
world.join({ userId: "foo" }, new Point(100, 100));
|
|
||||||
|
|
||||||
world.join({ userId: "bar" }, new Point(500, 100));
|
|
||||||
|
|
||||||
world.updatePosition({ userId: "bar" }, new Point(261, 100));
|
|
||||||
|
|
||||||
expect(connectCalledNumber).toBe(0);
|
|
||||||
|
|
||||||
world.updatePosition({ userId: "bar" }, new Point(101, 100));
|
|
||||||
|
|
||||||
expect(connectCalledNumber).toBe(2);
|
|
||||||
|
|
||||||
world.updatePosition({ userId: "bar" }, new Point(102, 100));
|
|
||||||
expect(connectCalledNumber).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should connect 3 users", () => {
|
|
||||||
let connectCalled: boolean = false;
|
|
||||||
const connect: ConnectCallback = (user: string, group: Group): void => {
|
|
||||||
connectCalled = true;
|
|
||||||
}
|
|
||||||
const disconnect: DisconnectCallback = (user: string, group: Group): void => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
|
||||||
|
|
||||||
world.join({ userId: "foo" }, new Point(100, 100));
|
|
||||||
|
|
||||||
world.join({ userId: "bar" }, new Point(200, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
|
||||||
connectCalled = false;
|
|
||||||
|
|
||||||
// baz joins at the outer limit of the group
|
|
||||||
world.join({ userId: "baz" }, new Point(311, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(false);
|
|
||||||
|
|
||||||
world.updatePosition({ userId: "baz" }, new Point(309, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should disconnect user1 and user2", () => {
|
|
||||||
let connectCalled: boolean = false;
|
|
||||||
let disconnectCallNumber: number = 0;
|
|
||||||
const connect: ConnectCallback = (user: string, group: Group): void => {
|
|
||||||
connectCalled = true;
|
|
||||||
}
|
|
||||||
const disconnect: DisconnectCallback = (user: string, group: Group): void => {
|
|
||||||
disconnectCallNumber++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const world = new World(connect, disconnect, 160, 160, () => {}, () => {});
|
|
||||||
|
|
||||||
world.join({ userId: "foo" }, new Point(100, 100));
|
|
||||||
|
|
||||||
world.join({ userId: "bar" }, new Point(259, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
|
||||||
expect(disconnectCallNumber).toBe(0);
|
|
||||||
|
|
||||||
world.updatePosition({ userId: "bar" }, new Point(100+160+160+1, 100));
|
|
||||||
|
|
||||||
expect(disconnectCallNumber).toBe(2);
|
|
||||||
|
|
||||||
world.updatePosition({ userId: "bar" }, new Point(262, 100));
|
|
||||||
expect(disconnectCallNumber).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
})
|
|
|
@ -4,14 +4,15 @@
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
|
"downlevelIteration": true,
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
"noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */ // Disabled because of sifrr server that is monkey patching HttpResponse
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
/* Additional Checks */
|
/* Additional Checks */
|
||||||
|
|
1940
back/yarn.lock
3
benchmark/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/node_modules/
|
||||||
|
/artillery_output.html
|
||||||
|
/artillery_output.json
|
69
benchmark/README.md
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Load testing
|
||||||
|
|
||||||
|
Load testing is performed with Artillery.
|
||||||
|
|
||||||
|
Install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd benchmark
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Running the tests (on one core):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd benchmark
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
You can adapt the `socketio-load-test.yaml` file to increase/decrease load.
|
||||||
|
|
||||||
|
Default settings are:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
phases:
|
||||||
|
- duration: 20
|
||||||
|
arrivalRate: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
which means: during 20 seconds, 2 users will be added every second (peaking at 40 simultaneous users).
|
||||||
|
|
||||||
|
Important: don't go above 40 simultaneous users for Artillery, otherwise, it is Artillery that will fail to run the tests properly.
|
||||||
|
To know, simply run "top". The "node" process for Artillery should never reach 100%.
|
||||||
|
|
||||||
|
Reports are generated in `artillery_output.html`.
|
||||||
|
|
||||||
|
# Multicore tests
|
||||||
|
|
||||||
|
You will want to test with Artillery running on multiple cores.
|
||||||
|
|
||||||
|
You can use
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./artillery_multi_core.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will trigger 4 Artillery instances in parallel.
|
||||||
|
|
||||||
|
Beware, the report generated is generated for only one instance.
|
||||||
|
|
||||||
|
# How to test, what to track?
|
||||||
|
|
||||||
|
While testing, you can check:
|
||||||
|
|
||||||
|
- CPU load of WorkAdventure API node process (it should not reach 100%)
|
||||||
|
- Get metrics at the end of the run: `http://api.workadventure.localhost/metrics`
|
||||||
|
In particular, look for:
|
||||||
|
```
|
||||||
|
# HELP nodejs_eventloop_lag_max_seconds The maximum recorded event loop delay.
|
||||||
|
# TYPE nodejs_eventloop_lag_max_seconds gauge
|
||||||
|
nodejs_eventloop_lag_max_seconds 23.991418879
|
||||||
|
```
|
||||||
|
This is the maximum time it took Node to process an event (you need to restart node after each test to reset this counter)
|
||||||
|
- Generate a profiling using "node --prof" by switching the command in docker-compose.yaml:
|
||||||
|
```
|
||||||
|
#command: yarn dev
|
||||||
|
command: yarn run profile
|
||||||
|
```
|
||||||
|
Read https://nodejs.org/en/docs/guides/simple-profiling/ on how to generate a profile.
|
||||||
|
|
15
benchmark/benchmark_multi_core.sh
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
yarn run start &
|
||||||
|
pid1=$!
|
||||||
|
yarn run start &
|
||||||
|
pid2=$!
|
||||||
|
yarn run start &
|
||||||
|
pid3=$!
|
||||||
|
yarn run start &
|
||||||
|
pid4=$!
|
||||||
|
|
||||||
|
wait $pid1
|
||||||
|
wait $pid2
|
||||||
|
wait $pid3
|
||||||
|
wait $pid4
|
60
benchmark/index.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import {RoomConnection} from "../front/src/Connexion/RoomConnection";
|
||||||
|
import {connectionManager} from "../front/src/Connexion/ConnectionManager";
|
||||||
|
import * as WebSocket from "ws"
|
||||||
|
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomConnection.setWebsocketFactory((url: string) => {
|
||||||
|
return new WebSocket(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function startOneUser(): Promise<void> {
|
||||||
|
await connectionManager.anonymousLogin(true);
|
||||||
|
const connection = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
||||||
|
{
|
||||||
|
x: 783,
|
||||||
|
y: 170
|
||||||
|
}, {
|
||||||
|
top: 0,
|
||||||
|
bottom: 200,
|
||||||
|
left: 500,
|
||||||
|
right: 800
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(connection.getUserId());
|
||||||
|
|
||||||
|
let angle = Math.random() * Math.PI * 2;
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const x = Math.floor(320 + 1472/2 * (1 + Math.sin(angle)));
|
||||||
|
const y = Math.floor(200 + 1090/2 * (1 + Math.cos(angle)));
|
||||||
|
|
||||||
|
connection.sharePosition(x, y, 'down', true, {
|
||||||
|
top: y - 200,
|
||||||
|
bottom: y + 200,
|
||||||
|
left: x - 320,
|
||||||
|
right: x + 320
|
||||||
|
})
|
||||||
|
|
||||||
|
angle += 0.05;
|
||||||
|
|
||||||
|
await sleep(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(10000);
|
||||||
|
connection.closeConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
connectionManager.initBenchmark();
|
||||||
|
|
||||||
|
|
||||||
|
for (let userNo = 0; userNo < 160; userNo++) {
|
||||||
|
startOneUser();
|
||||||
|
// Wait 0.5s between adding users
|
||||||
|
await sleep(125);
|
||||||
|
}
|
||||||
|
})();
|
706
benchmark/package-lock.json
generated
Normal file
|
@ -0,0 +1,706 @@
|
||||||
|
{
|
||||||
|
"name": "workadventure-artillery",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": {
|
||||||
|
"version": "14.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz",
|
||||||
|
"integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ=="
|
||||||
|
},
|
||||||
|
"@types/strip-bom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
|
"integrity": "sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I="
|
||||||
|
},
|
||||||
|
"@types/strip-json-comments": {
|
||||||
|
"version": "0.0.30",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz",
|
||||||
|
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ=="
|
||||||
|
},
|
||||||
|
"@types/ws": {
|
||||||
|
"version": "7.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.7.tgz",
|
||||||
|
"integrity": "sha512-UUFC/xxqFLP17hTva8/lVT0SybLUrfSD9c+iapKb0fEiC8uoDbA+xuZ3pAN603eW+bY8ebSMLm9jXdIPnD0ZgA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anymatch": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
|
||||||
|
"requires": {
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"picomatch": "^2.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||||
|
},
|
||||||
|
"array-find-index": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E="
|
||||||
|
},
|
||||||
|
"balanced-match": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||||
|
},
|
||||||
|
"binary-extensions": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
|
||||||
|
},
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"braces": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||||
|
"requires": {
|
||||||
|
"fill-range": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer-from": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||||
|
},
|
||||||
|
"camelcase-keys": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
|
||||||
|
"requires": {
|
||||||
|
"camelcase": "^2.0.0",
|
||||||
|
"map-obj": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
|
||||||
|
"integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chokidar": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==",
|
||||||
|
"requires": {
|
||||||
|
"anymatch": "~3.1.1",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"fsevents": "~2.1.2",
|
||||||
|
"glob-parent": "~5.1.0",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||||
|
},
|
||||||
|
"currently-unhandled": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
||||||
|
"integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
|
||||||
|
"requires": {
|
||||||
|
"array-find-index": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dateformat": {
|
||||||
|
"version": "1.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
|
||||||
|
"integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
|
||||||
|
"requires": {
|
||||||
|
"get-stdin": "^4.0.1",
|
||||||
|
"meow": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||||
|
},
|
||||||
|
"diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
|
||||||
|
},
|
||||||
|
"dynamic-dedupe": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz",
|
||||||
|
"integrity": "sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE=",
|
||||||
|
"requires": {
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error-ex": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
||||||
|
"requires": {
|
||||||
|
"is-arrayish": "^0.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fill-range": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||||
|
"requires": {
|
||||||
|
"to-regex-range": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"find-up": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
|
||||||
|
"requires": {
|
||||||
|
"path-exists": "^2.0.0",
|
||||||
|
"pinkie-promise": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||||
|
},
|
||||||
|
"fsevents": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"function-bind": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||||
|
},
|
||||||
|
"get-stdin": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
|
||||||
|
"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
|
||||||
|
},
|
||||||
|
"glob": {
|
||||||
|
"version": "7.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
|
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glob-parent": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
||||||
|
"requires": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"graceful-fs": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
|
||||||
|
},
|
||||||
|
"has": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
|
"requires": {
|
||||||
|
"function-bind": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hosted-git-info": {
|
||||||
|
"version": "2.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||||
|
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
||||||
|
},
|
||||||
|
"indent-string": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
|
||||||
|
"requires": {
|
||||||
|
"repeating": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||||
|
"requires": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
|
},
|
||||||
|
"is-arrayish": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
|
||||||
|
},
|
||||||
|
"is-binary-path": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"requires": {
|
||||||
|
"binary-extensions": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-core-module": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw==",
|
||||||
|
"requires": {
|
||||||
|
"has": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-extglob": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
|
||||||
|
},
|
||||||
|
"is-finite": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w=="
|
||||||
|
},
|
||||||
|
"is-glob": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
||||||
|
"requires": {
|
||||||
|
"is-extglob": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-number": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||||
|
},
|
||||||
|
"is-utf8": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
|
||||||
|
},
|
||||||
|
"load-json-file": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"parse-json": "^2.2.0",
|
||||||
|
"pify": "^2.0.0",
|
||||||
|
"pinkie-promise": "^2.0.0",
|
||||||
|
"strip-bom": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pify": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
|
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loud-rejection": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
||||||
|
"integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
|
||||||
|
"requires": {
|
||||||
|
"currently-unhandled": "^0.4.1",
|
||||||
|
"signal-exit": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||||
|
},
|
||||||
|
"map-obj": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
|
||||||
|
},
|
||||||
|
"meow": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
|
||||||
|
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
|
||||||
|
"requires": {
|
||||||
|
"camelcase-keys": "^2.0.0",
|
||||||
|
"decamelize": "^1.1.2",
|
||||||
|
"loud-rejection": "^1.0.0",
|
||||||
|
"map-obj": "^1.0.1",
|
||||||
|
"minimist": "^1.1.3",
|
||||||
|
"normalize-package-data": "^2.3.4",
|
||||||
|
"object-assign": "^4.0.1",
|
||||||
|
"read-pkg-up": "^1.0.1",
|
||||||
|
"redent": "^1.0.0",
|
||||||
|
"trim-newlines": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||||
|
},
|
||||||
|
"mkdirp": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||||
|
},
|
||||||
|
"normalize-package-data": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||||
|
"requires": {
|
||||||
|
"hosted-git-info": "^2.1.4",
|
||||||
|
"resolve": "^1.10.0",
|
||||||
|
"semver": "2 || 3 || 4 || 5",
|
||||||
|
"validate-npm-package-license": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"normalize-path": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
||||||
|
},
|
||||||
|
"object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||||
|
},
|
||||||
|
"once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||||
|
"requires": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parse-json": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
|
||||||
|
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
|
||||||
|
"requires": {
|
||||||
|
"error-ex": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-exists": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
|
||||||
|
"requires": {
|
||||||
|
"pinkie-promise": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||||
|
},
|
||||||
|
"path-parse": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
|
||||||
|
},
|
||||||
|
"path-type": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"pify": "^2.0.0",
|
||||||
|
"pinkie-promise": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pify": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
|
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"picomatch": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
|
||||||
|
},
|
||||||
|
"pinkie": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
||||||
|
"integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
|
||||||
|
},
|
||||||
|
"pinkie-promise": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
|
||||||
|
"requires": {
|
||||||
|
"pinkie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"read-pkg": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
|
||||||
|
"requires": {
|
||||||
|
"load-json-file": "^1.0.0",
|
||||||
|
"normalize-package-data": "^2.3.2",
|
||||||
|
"path-type": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"read-pkg-up": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
|
||||||
|
"requires": {
|
||||||
|
"find-up": "^1.0.0",
|
||||||
|
"read-pkg": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"readdirp": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
|
||||||
|
"requires": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redent": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
|
||||||
|
"requires": {
|
||||||
|
"indent-string": "^2.1.0",
|
||||||
|
"strip-indent": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repeating": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
|
||||||
|
"requires": {
|
||||||
|
"is-finite": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resolve": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==",
|
||||||
|
"requires": {
|
||||||
|
"is-core-module": "^2.0.0",
|
||||||
|
"path-parse": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rimraf": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||||
|
},
|
||||||
|
"signal-exit": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||||
|
},
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||||
|
},
|
||||||
|
"source-map-support": {
|
||||||
|
"version": "0.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
|
||||||
|
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
|
||||||
|
"requires": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"source-map": "^0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spdx-correct": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
|
||||||
|
"requires": {
|
||||||
|
"spdx-expression-parse": "^3.0.0",
|
||||||
|
"spdx-license-ids": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spdx-exceptions": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A=="
|
||||||
|
},
|
||||||
|
"spdx-expression-parse": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||||
|
"requires": {
|
||||||
|
"spdx-exceptions": "^2.1.0",
|
||||||
|
"spdx-license-ids": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"spdx-license-ids": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw=="
|
||||||
|
},
|
||||||
|
"strip-bom": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
|
||||||
|
"requires": {
|
||||||
|
"is-utf8": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strip-indent": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
|
||||||
|
"requires": {
|
||||||
|
"get-stdin": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
|
||||||
|
},
|
||||||
|
"to-regex-range": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"requires": {
|
||||||
|
"is-number": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tree-kill": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
|
||||||
|
},
|
||||||
|
"trim-newlines": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM="
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==",
|
||||||
|
"requires": {
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"source-map-support": "^0.5.17",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ts-node-dev": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-leA/3TgGtnVU77fGngBwVZztqyDRXirytR7dMtMWZS5b2hGpLl+VDnB0F/gf3A+HEPSzS/KwxgXFP7/LtgX4MQ==",
|
||||||
|
"requires": {
|
||||||
|
"chokidar": "^3.4.0",
|
||||||
|
"dateformat": "~1.0.4-1.2.3",
|
||||||
|
"dynamic-dedupe": "^0.3.0",
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"mkdirp": "^1.0.4",
|
||||||
|
"resolve": "^1.0.0",
|
||||||
|
"rimraf": "^2.6.1",
|
||||||
|
"source-map-support": "^0.5.12",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
|
"ts-node": "^9.0.0",
|
||||||
|
"tsconfig": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tsconfig": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==",
|
||||||
|
"requires": {
|
||||||
|
"@types/strip-bom": "^3.0.0",
|
||||||
|
"@types/strip-json-comments": "0.0.30",
|
||||||
|
"strip-bom": "^3.0.0",
|
||||||
|
"strip-json-comments": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"strip-bom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
|
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg=="
|
||||||
|
},
|
||||||
|
"validate-npm-package-license": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||||
|
"requires": {
|
||||||
|
"spdx-correct": "^3.0.0",
|
||||||
|
"spdx-expression-parse": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
|
||||||
|
},
|
||||||
|
"xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||||
|
},
|
||||||
|
"yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
benchmark/package.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "workadventure-artillery",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Load testing for WorkAdventure",
|
||||||
|
"scripts": {
|
||||||
|
"start": "ts-node ./index.ts"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Grégoire Parant",
|
||||||
|
"email": "g.parant@thecodingmachine.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "David Négrier",
|
||||||
|
"email": "d.negrier@thecodingmachine.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Arthmaël Poly",
|
||||||
|
"email": "a.poly@thecodingmachine.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "SEE LICENSE IN LICENSE.txt",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ws": "^7.2.6",
|
||||||
|
"ts-node-dev": "^1.0.0-pre.62",
|
||||||
|
"typescript": "^4.0.2",
|
||||||
|
"ws": "^7.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {}
|
||||||
|
}
|
528
benchmark/yarn.lock
Normal file
|
@ -0,0 +1,528 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/node@*":
|
||||||
|
version "14.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
|
||||||
|
|
||||||
|
"@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"
|
||||||
|
|
||||||
|
"@types/strip-json-comments@0.0.30":
|
||||||
|
version "0.0.30"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1"
|
||||||
|
|
||||||
|
"@types/ws@^7.2.6":
|
||||||
|
version "7.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d"
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
anymatch@~3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
|
||||||
|
dependencies:
|
||||||
|
normalize-path "^3.0.0"
|
||||||
|
picomatch "^2.0.4"
|
||||||
|
|
||||||
|
arg@^4.1.0:
|
||||||
|
version "4.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||||
|
|
||||||
|
array-find-index@^1.0.1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
|
||||||
|
|
||||||
|
balanced-match@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||||
|
|
||||||
|
binary-extensions@^2.0.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
|
||||||
|
|
||||||
|
brace-expansion@^1.1.7:
|
||||||
|
version "1.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||||
|
dependencies:
|
||||||
|
balanced-match "^1.0.0"
|
||||||
|
concat-map "0.0.1"
|
||||||
|
|
||||||
|
braces@~3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||||
|
dependencies:
|
||||||
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
|
buffer-from@^1.0.0:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||||
|
|
||||||
|
camelcase-keys@^2.0.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
|
||||||
|
dependencies:
|
||||||
|
camelcase "^2.0.0"
|
||||||
|
map-obj "^1.0.0"
|
||||||
|
|
||||||
|
camelcase@^2.0.0:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
|
||||||
|
|
||||||
|
chokidar@^3.4.0:
|
||||||
|
version "3.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d"
|
||||||
|
dependencies:
|
||||||
|
anymatch "~3.1.1"
|
||||||
|
braces "~3.0.2"
|
||||||
|
glob-parent "~5.1.0"
|
||||||
|
is-binary-path "~2.1.0"
|
||||||
|
is-glob "~4.0.1"
|
||||||
|
normalize-path "~3.0.0"
|
||||||
|
readdirp "~3.4.0"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.1.2"
|
||||||
|
|
||||||
|
concat-map@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
|
||||||
|
currently-unhandled@^0.4.1:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
|
||||||
|
dependencies:
|
||||||
|
array-find-index "^1.0.1"
|
||||||
|
|
||||||
|
dateformat@~1.0.4-1.2.3:
|
||||||
|
version "1.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9"
|
||||||
|
dependencies:
|
||||||
|
get-stdin "^4.0.1"
|
||||||
|
meow "^3.3.0"
|
||||||
|
|
||||||
|
decamelize@^1.1.2:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||||
|
|
||||||
|
diff@^4.0.1:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||||
|
|
||||||
|
dynamic-dedupe@^0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1"
|
||||||
|
dependencies:
|
||||||
|
xtend "^4.0.0"
|
||||||
|
|
||||||
|
error-ex@^1.2.0:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||||
|
dependencies:
|
||||||
|
is-arrayish "^0.2.1"
|
||||||
|
|
||||||
|
fill-range@^7.0.1:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||||
|
dependencies:
|
||||||
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
|
find-up@^1.0.0:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
|
||||||
|
dependencies:
|
||||||
|
path-exists "^2.0.0"
|
||||||
|
pinkie-promise "^2.0.0"
|
||||||
|
|
||||||
|
fs.realpath@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
|
|
||||||
|
fsevents@~2.1.2:
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
|
||||||
|
|
||||||
|
get-stdin@^4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||||
|
|
||||||
|
glob-parent@~5.1.0:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
|
||||||
|
dependencies:
|
||||||
|
is-glob "^4.0.1"
|
||||||
|
|
||||||
|
glob@^7.1.3:
|
||||||
|
version "7.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
||||||
|
dependencies:
|
||||||
|
fs.realpath "^1.0.0"
|
||||||
|
inflight "^1.0.4"
|
||||||
|
inherits "2"
|
||||||
|
minimatch "^3.0.4"
|
||||||
|
once "^1.3.0"
|
||||||
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
|
graceful-fs@^4.1.2:
|
||||||
|
version "4.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
|
|
||||||
|
hosted-git-info@^2.1.4:
|
||||||
|
version "2.8.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||||
|
|
||||||
|
indent-string@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
|
||||||
|
dependencies:
|
||||||
|
repeating "^2.0.0"
|
||||||
|
|
||||||
|
inflight@^1.0.4:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||||
|
dependencies:
|
||||||
|
once "^1.3.0"
|
||||||
|
wrappy "1"
|
||||||
|
|
||||||
|
inherits@2:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
|
||||||
|
is-arrayish@^0.2.1:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||||
|
|
||||||
|
is-binary-path@~2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||||
|
dependencies:
|
||||||
|
binary-extensions "^2.0.0"
|
||||||
|
|
||||||
|
is-extglob@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||||
|
|
||||||
|
is-finite@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
|
||||||
|
|
||||||
|
is-glob@^4.0.1, is-glob@~4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
|
||||||
|
dependencies:
|
||||||
|
is-extglob "^2.1.1"
|
||||||
|
|
||||||
|
is-number@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||||
|
|
||||||
|
is-utf8@^0.2.0:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||||
|
|
||||||
|
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"
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
parse-json "^2.2.0"
|
||||||
|
pify "^2.0.0"
|
||||||
|
pinkie-promise "^2.0.0"
|
||||||
|
strip-bom "^2.0.0"
|
||||||
|
|
||||||
|
loud-rejection@^1.0.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
|
||||||
|
dependencies:
|
||||||
|
currently-unhandled "^0.4.1"
|
||||||
|
signal-exit "^3.0.0"
|
||||||
|
|
||||||
|
make-error@^1.1.1:
|
||||||
|
version "1.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||||
|
|
||||||
|
map-obj@^1.0.0, map-obj@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
||||||
|
|
||||||
|
meow@^3.3.0:
|
||||||
|
version "3.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
|
||||||
|
dependencies:
|
||||||
|
camelcase-keys "^2.0.0"
|
||||||
|
decamelize "^1.1.2"
|
||||||
|
loud-rejection "^1.0.0"
|
||||||
|
map-obj "^1.0.1"
|
||||||
|
minimist "^1.1.3"
|
||||||
|
normalize-package-data "^2.3.4"
|
||||||
|
object-assign "^4.0.1"
|
||||||
|
read-pkg-up "^1.0.1"
|
||||||
|
redent "^1.0.0"
|
||||||
|
trim-newlines "^1.0.0"
|
||||||
|
|
||||||
|
minimatch@^3.0.4:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||||
|
dependencies:
|
||||||
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
|
minimist@^1.1.3, minimist@^1.2.5:
|
||||||
|
version "1.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
|
|
||||||
|
mkdirp@^1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
|
|
||||||
|
normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
|
||||||
|
dependencies:
|
||||||
|
hosted-git-info "^2.1.4"
|
||||||
|
resolve "^1.10.0"
|
||||||
|
semver "2 || 3 || 4 || 5"
|
||||||
|
validate-npm-package-license "^3.0.1"
|
||||||
|
|
||||||
|
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
|
|
||||||
|
object-assign@^4.0.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
|
|
||||||
|
once@^1.3.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||||
|
dependencies:
|
||||||
|
wrappy "1"
|
||||||
|
|
||||||
|
parse-json@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
|
||||||
|
dependencies:
|
||||||
|
error-ex "^1.2.0"
|
||||||
|
|
||||||
|
path-exists@^2.0.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
|
||||||
|
dependencies:
|
||||||
|
pinkie-promise "^2.0.0"
|
||||||
|
|
||||||
|
path-is-absolute@^1.0.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||||
|
|
||||||
|
path-parse@^1.0.6:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
|
||||||
|
|
||||||
|
path-type@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
pify "^2.0.0"
|
||||||
|
pinkie-promise "^2.0.0"
|
||||||
|
|
||||||
|
picomatch@^2.0.4, picomatch@^2.2.1:
|
||||||
|
version "2.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
|
||||||
|
|
||||||
|
pify@^2.0.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
|
|
||||||
|
pinkie-promise@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
|
||||||
|
dependencies:
|
||||||
|
pinkie "^2.0.0"
|
||||||
|
|
||||||
|
pinkie@^2.0.0:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
|
||||||
|
|
||||||
|
read-pkg-up@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
|
||||||
|
dependencies:
|
||||||
|
find-up "^1.0.0"
|
||||||
|
read-pkg "^1.0.0"
|
||||||
|
|
||||||
|
read-pkg@^1.0.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
|
||||||
|
dependencies:
|
||||||
|
load-json-file "^1.0.0"
|
||||||
|
normalize-package-data "^2.3.2"
|
||||||
|
path-type "^1.0.0"
|
||||||
|
|
||||||
|
readdirp@~3.4.0:
|
||||||
|
version "3.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
|
||||||
|
dependencies:
|
||||||
|
picomatch "^2.2.1"
|
||||||
|
|
||||||
|
redent@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
|
||||||
|
dependencies:
|
||||||
|
indent-string "^2.1.0"
|
||||||
|
strip-indent "^1.0.1"
|
||||||
|
|
||||||
|
repeating@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
|
||||||
|
dependencies:
|
||||||
|
is-finite "^1.0.0"
|
||||||
|
|
||||||
|
resolve@^1.0.0, resolve@^1.10.0:
|
||||||
|
version "1.17.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
|
||||||
|
dependencies:
|
||||||
|
path-parse "^1.0.6"
|
||||||
|
|
||||||
|
rimraf@^2.6.1:
|
||||||
|
version "2.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||||
|
dependencies:
|
||||||
|
glob "^7.1.3"
|
||||||
|
|
||||||
|
"semver@2 || 3 || 4 || 5":
|
||||||
|
version "5.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
|
|
||||||
|
signal-exit@^3.0.0:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
|
|
||||||
|
source-map-support@^0.5.12, source-map-support@^0.5.17:
|
||||||
|
version "0.5.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
||||||
|
dependencies:
|
||||||
|
buffer-from "^1.0.0"
|
||||||
|
source-map "^0.6.0"
|
||||||
|
|
||||||
|
source-map@^0.6.0:
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||||
|
|
||||||
|
spdx-correct@^3.0.0:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
||||||
|
dependencies:
|
||||||
|
spdx-expression-parse "^3.0.0"
|
||||||
|
spdx-license-ids "^3.0.0"
|
||||||
|
|
||||||
|
spdx-exceptions@^2.1.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
|
||||||
|
|
||||||
|
spdx-expression-parse@^3.0.0:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
|
||||||
|
dependencies:
|
||||||
|
spdx-exceptions "^2.1.0"
|
||||||
|
spdx-license-ids "^3.0.0"
|
||||||
|
|
||||||
|
spdx-license-ids@^3.0.0:
|
||||||
|
version "3.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
|
||||||
|
|
||||||
|
strip-bom@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
|
||||||
|
dependencies:
|
||||||
|
is-utf8 "^0.2.0"
|
||||||
|
|
||||||
|
strip-bom@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||||
|
|
||||||
|
strip-indent@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
|
||||||
|
dependencies:
|
||||||
|
get-stdin "^4.0.1"
|
||||||
|
|
||||||
|
strip-json-comments@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
|
|
||||||
|
to-regex-range@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||||
|
dependencies:
|
||||||
|
is-number "^7.0.0"
|
||||||
|
|
||||||
|
tree-kill@^1.2.2:
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||||
|
|
||||||
|
trim-newlines@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
||||||
|
|
||||||
|
ts-node-dev@^1.0.0-pre.62:
|
||||||
|
version "1.0.0-pre.62"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.62.tgz#835644c43669b659a880379b9d06df86cef665ad"
|
||||||
|
dependencies:
|
||||||
|
chokidar "^3.4.0"
|
||||||
|
dateformat "~1.0.4-1.2.3"
|
||||||
|
dynamic-dedupe "^0.3.0"
|
||||||
|
minimist "^1.2.5"
|
||||||
|
mkdirp "^1.0.4"
|
||||||
|
resolve "^1.0.0"
|
||||||
|
rimraf "^2.6.1"
|
||||||
|
source-map-support "^0.5.12"
|
||||||
|
tree-kill "^1.2.2"
|
||||||
|
ts-node "^8.10.2"
|
||||||
|
tsconfig "^7.0.0"
|
||||||
|
|
||||||
|
ts-node@^8.10.2:
|
||||||
|
version "8.10.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
|
||||||
|
dependencies:
|
||||||
|
arg "^4.1.0"
|
||||||
|
diff "^4.0.1"
|
||||||
|
make-error "^1.1.1"
|
||||||
|
source-map-support "^0.5.17"
|
||||||
|
yn "3.1.1"
|
||||||
|
|
||||||
|
tsconfig@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7"
|
||||||
|
dependencies:
|
||||||
|
"@types/strip-bom" "^3.0.0"
|
||||||
|
"@types/strip-json-comments" "0.0.30"
|
||||||
|
strip-bom "^3.0.0"
|
||||||
|
strip-json-comments "^2.0.0"
|
||||||
|
|
||||||
|
typescript@^4.0.2:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
|
||||||
|
|
||||||
|
validate-npm-package-license@^3.0.1:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
||||||
|
dependencies:
|
||||||
|
spdx-correct "^3.0.0"
|
||||||
|
spdx-expression-parse "^3.0.0"
|
||||||
|
|
||||||
|
wrappy@1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
|
||||||
|
ws@^7.3.1:
|
||||||
|
version "7.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
||||||
|
|
||||||
|
xtend@^4.0.0:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
|
|
||||||
|
yn@3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
|
@ -4,6 +4,7 @@
|
||||||
local tag = namespace,
|
local tag = namespace,
|
||||||
local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com",
|
local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com",
|
||||||
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
||||||
|
"version": "1.0",
|
||||||
"containers": {
|
"containers": {
|
||||||
"back": {
|
"back": {
|
||||||
"image": "thecodingmachine/workadventure-back:"+tag,
|
"image": "thecodingmachine/workadventure-back:"+tag,
|
||||||
|
@ -13,7 +14,12 @@
|
||||||
},
|
},
|
||||||
"ports": [8080],
|
"ports": [8080],
|
||||||
"env": {
|
"env": {
|
||||||
"SECRET_KEY": "tempSecretKeyNeedsToChange"
|
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
||||||
|
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
||||||
|
"ADMIN_API_URL": "https://admin."+url,
|
||||||
|
"JITSI_ISS": env.JITSI_ISS,
|
||||||
|
"JITSI_URL": env.JITSI_URL,
|
||||||
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"front": {
|
"front": {
|
||||||
|
@ -24,9 +30,23 @@
|
||||||
},
|
},
|
||||||
"ports": [80],
|
"ports": [80],
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": "https://api."+url
|
"API_URL": "api."+url,
|
||||||
|
"JITSI_URL": env.JITSI_URL,
|
||||||
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
|
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
|
||||||
|
"TURN_USER": "workadventure",
|
||||||
|
"TURN_PASSWORD": "WorkAdventure123",
|
||||||
|
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"maps": {
|
||||||
|
"image": "thecodingmachine/workadventure-maps:"+tag,
|
||||||
|
"host": {
|
||||||
|
"url": "maps."+url,
|
||||||
|
"https": "enable"
|
||||||
|
},
|
||||||
|
"ports": [80]
|
||||||
|
},
|
||||||
"website": {
|
"website": {
|
||||||
"image": "thecodingmachine/workadventure-website:"+tag,
|
"image": "thecodingmachine/workadventure-website:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
|
@ -42,6 +62,23 @@
|
||||||
"config": {
|
"config": {
|
||||||
"https": {
|
"https": {
|
||||||
"mail": "d.negrier@thecodingmachine.com"
|
"mail": "d.negrier@thecodingmachine.com"
|
||||||
}
|
},
|
||||||
|
k8sextension(k8sConf)::
|
||||||
|
k8sConf + {
|
||||||
|
back+: {
|
||||||
|
deployment+: {
|
||||||
|
spec+: {
|
||||||
|
template+: {
|
||||||
|
metadata+: {
|
||||||
|
annotations+: {
|
||||||
|
"prometheus.io/port": "8080",
|
||||||
|
"prometheus.io/scrape": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,14 @@ version: "3"
|
||||||
services:
|
services:
|
||||||
reverse-proxy:
|
reverse-proxy:
|
||||||
image: traefik:v2.0
|
image: traefik:v2.0
|
||||||
command: --api.insecure=true --providers.docker
|
command:
|
||||||
|
- --api.insecure=true
|
||||||
|
- --providers.docker
|
||||||
|
- --entryPoints.web.address=:80
|
||||||
|
- --entryPoints.websecure.address=:443
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
# The Web UI (enabled by --api.insecure=true)
|
# The Web UI (enabled by --api.insecure=true)
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@ -14,19 +19,52 @@ services:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
front:
|
front:
|
||||||
image: thecodingmachine/nodejs:12
|
image: thecodingmachine/nodejs:14
|
||||||
environment:
|
environment:
|
||||||
DEBUG_MODE: "$DEBUG_MODE"
|
DEBUG_MODE: "$DEBUG_MODE"
|
||||||
|
JITSI_URL: $JITSI_URL
|
||||||
|
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
|
||||||
HOST: "0.0.0.0"
|
HOST: "0.0.0.0"
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
API_URL: http://api.workadventure.localhost
|
API_URL: api.workadventure.localhost
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
|
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
|
||||||
|
TURN_USER: workadventure
|
||||||
|
TURN_PASSWORD: WorkAdventure123
|
||||||
command: yarn run start
|
command: yarn run start
|
||||||
volumes:
|
volumes:
|
||||||
- ./front:/usr/src/app
|
- ./front:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
|
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.front.entryPoints=web,traefik"
|
||||||
- "traefik.http.services.front.loadbalancer.server.port=8080"
|
- "traefik.http.services.front.loadbalancer.server.port=8080"
|
||||||
|
- "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.front-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.front-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.front-ssl.service=front"
|
||||||
|
|
||||||
|
maps:
|
||||||
|
image: thecodingmachine/nodejs:12-apache
|
||||||
|
environment:
|
||||||
|
DEBUG_MODE: "$DEBUG_MODE"
|
||||||
|
HOST: "0.0.0.0"
|
||||||
|
NODE_ENV: development
|
||||||
|
#APACHE_DOCUMENT_ROOT: dist/
|
||||||
|
#APACHE_EXTENSIONS: headers
|
||||||
|
#APACHE_EXTENSION_HEADERS: 1
|
||||||
|
STARTUP_COMMAND_0: sudo a2enmod headers
|
||||||
|
STARTUP_COMMAND_1: yarn install
|
||||||
|
STARTUP_COMMAND_2: yarn run dev &
|
||||||
|
volumes:
|
||||||
|
- ./maps:/var/www/html
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.maps.entryPoints=web,traefik"
|
||||||
|
- "traefik.http.services.maps.loadbalancer.server.port=80"
|
||||||
|
- "traefik.http.routers.maps-ssl.rule=Host(`maps.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.maps-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.maps-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.maps-ssl.service=maps"
|
||||||
|
|
||||||
back:
|
back:
|
||||||
image: thecodingmachine/nodejs:12
|
image: thecodingmachine/nodejs:12
|
||||||
|
@ -35,11 +73,22 @@ services:
|
||||||
environment:
|
environment:
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
|
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||||
|
ALLOW_ARTILLERY: "true"
|
||||||
|
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
||||||
|
JITSI_URL: $JITSI_URL
|
||||||
|
JITSI_ISS: $JITSI_ISS
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)"
|
- "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.back.entryPoints=web"
|
||||||
- "traefik.http.services.back.loadbalancer.server.port=8080"
|
- "traefik.http.services.back.loadbalancer.server.port=8080"
|
||||||
|
- "traefik.http.routers.back-ssl.rule=Host(`api.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.back-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.back-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.back-ssl.service=back"
|
||||||
|
|
||||||
|
|
||||||
website:
|
website:
|
||||||
image: thecodingmachine/nodejs:12-apache
|
image: thecodingmachine/nodejs:12-apache
|
||||||
|
@ -51,4 +100,19 @@ services:
|
||||||
- ./website:/var/www/html
|
- ./website:/var/www/html
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.website.rule=Host(`workadventure.localhost`)"
|
- "traefik.http.routers.website.rule=Host(`workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.website.entryPoints=web"
|
||||||
- "traefik.http.services.website.loadbalancer.server.port=80"
|
- "traefik.http.services.website.loadbalancer.server.port=80"
|
||||||
|
- "traefik.http.routers.website-ssl.rule=Host(`workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.website-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.website-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.website-ssl.service=website"
|
||||||
|
|
||||||
|
messages:
|
||||||
|
image: thecodingmachine/workadventure-back-base:latest
|
||||||
|
environment:
|
||||||
|
STARTUP_COMMAND_1: yarn install
|
||||||
|
STARTUP_COMMAND_2: yarn run proto:watch
|
||||||
|
volumes:
|
||||||
|
- ./messages:/usr/src/app
|
||||||
|
- ./back:/usr/src/back
|
||||||
|
- ./front:/usr/src/front
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/eslint-recommended"
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
||||||
],
|
],
|
||||||
"globals": {
|
"globals": {
|
||||||
"Atomics": "readonly",
|
"Atomics": "readonly",
|
||||||
|
@ -16,12 +17,14 @@
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2018,
|
"ecmaVersion": 2018,
|
||||||
"sourceType": "module"
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@typescript-eslint"
|
"@typescript-eslint"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off"
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
front/.gitignore
vendored
|
@ -4,3 +4,4 @@
|
||||||
/dist/webpack.config.js
|
/dist/webpack.config.js
|
||||||
/dist/webpack.config.js.map
|
/dist/webpack.config.js.map
|
||||||
/dist/src
|
/dist/src
|
||||||
|
*.sh
|
|
@ -1,7 +1,13 @@
|
||||||
# we are rebuilding on each deploy to cope with the API_URL environment URL
|
FROM thecodingmachine/workadventure-back-base:latest as builder
|
||||||
FROM thecodingmachine/nodejs:12-apache
|
WORKDIR /var/www/messages
|
||||||
|
COPY --chown=docker:docker messages .
|
||||||
|
RUN yarn install && yarn proto
|
||||||
|
|
||||||
COPY --chown=docker:docker . .
|
# we are rebuilding on each deploy to cope with the API_URL environment URL
|
||||||
|
FROM thecodingmachine/nodejs:14-apache
|
||||||
|
|
||||||
|
COPY --chown=docker:docker front .
|
||||||
|
COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
3
front/dist/.htaccess
vendored
|
@ -20,4 +20,5 @@ RewriteBase /
|
||||||
# We only want to let Apache serve files and not directories.
|
# We only want to let Apache serve files and not directories.
|
||||||
# Rewrite all other queries starting with _ to index.ts.
|
# Rewrite all other queries starting with _ to index.ts.
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteRule "^_/" "/index.html" [L]
|
RewriteRule "^[_@]/" "/index.html" [L]
|
||||||
|
RewriteRule "^register/" "/index.html" [L]
|
||||||
|
|
59
front/dist/index.html
vendored
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
gtag('config', 'UA-10196481-11');
|
gtag('config', 'UA-10196481-11');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
|
<link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
|
<link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="static/images/favicons/apple-icon-72x72.png">
|
<link rel="apple-touch-icon" sizes="72x72" href="static/images/favicons/apple-icon-72x72.png">
|
||||||
|
@ -39,7 +40,45 @@
|
||||||
<title>WorkAdventure</title>
|
<title>WorkAdventure</title>
|
||||||
</head>
|
</head>
|
||||||
<body id="body" style="margin: 0">
|
<body id="body" style="margin: 0">
|
||||||
<div id="webRtc" class="webrtc">
|
<div class="main-container" id="main-container">
|
||||||
|
<!-- Create the editor container -->
|
||||||
|
<div id="game" class="game">
|
||||||
|
<div id="game-overlay" class="game-overlay">
|
||||||
|
<div id="main-section" class="main-section">
|
||||||
|
</div>
|
||||||
|
<aside id="sidebar" class="sidebar">
|
||||||
|
</aside>
|
||||||
|
<div id="chat-mode" class="chat-mode three-col" style="display: none;">
|
||||||
|
</div>
|
||||||
|
<div id="activeCam" class="activeCam">
|
||||||
|
<div id="div-myCamVideo" class="video-container">
|
||||||
|
<video id="myCamVideo" autoplay muted></video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-cam-action">
|
||||||
|
<div id="btn-micro" class="btn-micro">
|
||||||
|
<img id="microphone" src="resources/logos/microphone.svg">
|
||||||
|
<img id="microphone-close" src="resources/logos/microphone-close.svg">
|
||||||
|
</div>
|
||||||
|
<div id="btn-video" class="btn-video">
|
||||||
|
<img id="cinema" src="resources/logos/cinema.svg">
|
||||||
|
<img id="cinema-close" src="resources/logos/cinema-close.svg">
|
||||||
|
</div>
|
||||||
|
<div id="btn-monitor" class="btn-monitor">
|
||||||
|
<img id="monitor" src="resources/logos/monitor.svg">
|
||||||
|
<img id="monitor-close" src="resources/logos/monitor-close.svg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="cowebsite" class="cowebsite hidden"></div>
|
||||||
|
<div class="audio-playing">
|
||||||
|
<img src="/resources/logos/megaphone.svg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div id="webRtc" class="webrtc">
|
||||||
<div id="activeCam" class="activeCam">
|
<div id="activeCam" class="activeCam">
|
||||||
<div id="div-myCamVideo" class="video-container">
|
<div id="div-myCamVideo" class="video-container">
|
||||||
<video id="myCamVideo" autoplay muted></video>
|
<video id="myCamVideo" autoplay muted></video>
|
||||||
|
@ -54,13 +93,25 @@
|
||||||
<img id="cinema" src="resources/logos/CAM-ON.png">
|
<img id="cinema" src="resources/logos/CAM-ON.png">
|
||||||
<img id="cinema-close" src="resources/logos/CAM-OFF.png">
|
<img id="cinema-close" src="resources/logos/CAM-OFF.png">
|
||||||
</div>
|
</div>
|
||||||
<!--<div class="btn-call">
|
<div class="btn-monitor">
|
||||||
<img src="resources/logos/phone.svg">
|
<img id="monitor" src="resources/logos/monitor.svg">
|
||||||
</div>-->
|
<img id="monitor-close" src="resources/logos/monitor-close.svg">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
<div id="activeScreenSharing" class="active-screen-sharing active">
|
||||||
|
</div>
|
||||||
|
<div id="webRtcSetup" class="webrtcsetup">
|
||||||
|
<img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg">
|
||||||
|
<video id="myCamVideoSetup" autoplay muted></video>
|
||||||
|
</div>
|
||||||
<audio id="audio-webrtc-in">
|
<audio id="audio-webrtc-in">
|
||||||
<source src="/resources/objects/webrtc-in.mp3" type="audio/mp3">
|
<source src="/resources/objects/webrtc-in.mp3" type="audio/mp3">
|
||||||
</audio>
|
</audio>
|
||||||
|
<audio id="report-message">
|
||||||
|
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
|
||||||
|
</audio>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
BIN
front/dist/resources/customisation/character_accessories/character_accessories1.png
vendored
Normal file
After Width: | Height: | Size: 293 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories10.png
vendored
Normal file
After Width: | Height: | Size: 517 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories11.png
vendored
Normal file
After Width: | Height: | Size: 517 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories12.png
vendored
Normal file
After Width: | Height: | Size: 524 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories13.png
vendored
Normal file
After Width: | Height: | Size: 505 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories14.png
vendored
Normal file
After Width: | Height: | Size: 525 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories15.png
vendored
Normal file
After Width: | Height: | Size: 517 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories16.png
vendored
Normal file
After Width: | Height: | Size: 743 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories17.png
vendored
Normal file
After Width: | Height: | Size: 798 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories18.png
vendored
Normal file
After Width: | Height: | Size: 754 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories19.png
vendored
Normal file
After Width: | Height: | Size: 814 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories2.png
vendored
Normal file
After Width: | Height: | Size: 452 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories20.png
vendored
Normal file
After Width: | Height: | Size: 810 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories21.png
vendored
Normal file
After Width: | Height: | Size: 827 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories22.png
vendored
Normal file
After Width: | Height: | Size: 451 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories23.png
vendored
Normal file
After Width: | Height: | Size: 451 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories24.png
vendored
Normal file
After Width: | Height: | Size: 451 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories25.png
vendored
Normal file
After Width: | Height: | Size: 451 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories26.png
vendored
Normal file
After Width: | Height: | Size: 281 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories27.png
vendored
Normal file
After Width: | Height: | Size: 276 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories28.png
vendored
Normal file
After Width: | Height: | Size: 368 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories29.png
vendored
Normal file
After Width: | Height: | Size: 332 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories3.png
vendored
Normal file
After Width: | Height: | Size: 450 B |
BIN
front/dist/resources/customisation/character_accessories/character_accessories30.png
vendored
Normal file
After Width: | Height: | Size: 324 B |