Merge branch 'develop' into exitTriggerAction

# Conflicts:
#	front/src/Phaser/Game/GameScene.ts
This commit is contained in:
Gregoire Parant 2021-02-17 19:28:41 +01:00
commit 83fc7d0cc0
88 changed files with 1382 additions and 1996 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
**/node_modules/**
**/Dockerfile

View File

@ -6,3 +6,7 @@ JITSI_ISS=
SECRET_JITSI_KEY=
ADMIN_API_TOKEN=123
START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json
# If your Turn server is configured to use the Turn REST API, you should put the shared auth secret here.
# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
# Keep empty if you are sharing hard coded / clear text credentials.
TURN_STATIC_AUTH_SECRET=

View File

@ -150,6 +150,7 @@ jobs:
JITSI_ISS: ${{ secrets.JITSI_ISS }}
JITSI_URL: ${{ secrets.JITSI_URL }}
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
with:
namespace: workadventure-${{ env.GITHUB_REF_SLUG }}
@ -159,34 +160,5 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: Environment deployed at https://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
msg: Environment deployed at https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
check_for_duplicate_msg: true
- name: Run Cypress tests
uses: cypress-io/github-action@v2
if: ${{ env.GITHUB_REF_SLUG != 'master' }}
env:
CYPRESS_BASE_URL: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
with:
env: host=play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com,port=80
spec: cypress/integration/spec.js
wait-on: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
working-directory: e2e
- name: Run Cypress tests in prod
uses: cypress-io/github-action@v2
if: ${{ env.GITHUB_REF_SLUG == 'master' }}
env:
CYPRESS_BASE_URL: https://play.workadventu.re
with:
env: host=play.workadventu.re
spec: cypress/integration/spec.js
wait-on: https://workadventu.re
working-directory: e2e
- name: "Upload the screenshot on test failure"
uses: actions/upload-artifact@v1
if: failure()
with:
name: "screenshot"
path: "./e2e/cypress/screenshots/spec.js/WorkAdventureGame -- loads (failed).png"

View File

@ -6,8 +6,6 @@ Demo here : [https://workadventu.re/](https://workadventu.re/).
# Work Adventure
## Work in progress
Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a
16-bit video game.
@ -15,7 +13,7 @@ In Work Adventure, you can move around your office and talk to your colleagues (
triggered when you move next to a colleague).
## Getting started
## Setting up a development environment
Install Docker.
@ -101,5 +99,7 @@ Vagrant destroy
* `Vagrant halt`: stop your VM Vagrant.
* `Vagrant destroy`: delete your VM Vagrant.
## Features developed
You have more details of features developed in back [README.md](./back/README.md).
## Setting up a production environment
The way you set up your production environment will highly depend on your servers.
We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) directory.

View File

@ -1,16 +1,26 @@
FROM thecodingmachine/workadventure-back-base:latest as builder
WORKDIR /var/www/messages
COPY --chown=docker:docker messages .
# protobuf build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
WORKDIR /usr/src
COPY messages .
RUN yarn install && yarn proto
FROM thecodingmachine/nodejs:12
COPY --chown=docker:docker back .
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated
# typescript build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
WORKDIR /usr/src
COPY back/yarn.lock back/package.json ./
RUN yarn install
COPY back .
COPY --from=builder /usr/src/generated src/Messages/generated
ENV NODE_ENV=production
RUN yarn run tsc
CMD ["yarn", "run", "runprod"]
# final production image
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76
WORKDIR /usr/src
COPY back/yarn.lock back/package.json ./
COPY --from=builder2 /usr/src/dist /usr/src/dist
ENV NODE_ENV=production
RUN yarn install --production
USER node
CMD ["yarn", "run", "runprod"]

View File

@ -1,61 +0,0 @@
# Back Features
## Login
To start your game, you must authenticate on the server back.
When you are authenticated, the back server return token and room starting.
```
POST => /login
Params :
email: email of user.
```
## Join a room
When a user is connected, the user can join a room.
So you must send emit `join-room` with information user:
```
Socket.io => 'join-room'
userId: user id of gamer
roomId: room id when user enter in game
position: {
x: position x on map
y: position y on map
}
```
All data users are stocked on socket client.
## Send position user
When user move on the map, you can share new position on back with event `user-position`.
The information sent:
```
Socket.io => 'user-position'
userId: user id of gamer
roomId: room id when user enter in game
position: {
x: position x on map
y: position y on map
}
```
All data users are updated on socket client.
## Receive positions of all users
The application sends position of all users in each room in every few 10 milliseconds.
The data will pushed on event `user-position`:
```
Socket.io => 'user-position'
[
{
userId: user id of gamer
roomId: room id when user enter in game
position: {
x: position x on map
y: position y on map
}
},
...
]
```
[<<< back](../README.md)

View File

@ -1,4 +1,3 @@
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
@ -12,9 +11,9 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080;
const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051;
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || '';
export {
SECRET_KEY,
MINIMUM_DISTANCE,
ADMIN_API_URL,
ADMIN_API_TOKEN,

View File

@ -100,11 +100,12 @@ class AdminApi {
return res.data;
}
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) {
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) {
return Axios.post(`${ADMIN_API_URL}/api/report`, {
reportedUserUuid,
reportedUserComment,
reporterUserUuid,
reportWorldSlug,
},
{
headers: {"Authorization": `${ADMIN_API_TOKEN}`}

View File

@ -28,7 +28,13 @@ import {User, UserSocket} from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {Group} from "../Model/Group";
import {cpuTracker} from "./CpuTracker";
import {GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable";
import {
GROUP_RADIUS,
JITSI_ISS,
MINIMUM_DISTANCE,
SECRET_JITSI_KEY,
TURN_STATIC_AUTH_SECRET
} from "../Enum/EnvironmentVariable";
import {Movable} from "../Model/Movable";
import {PositionInterface} from "../Model/PositionInterface";
import {adminApi, CharacterTexture} from "./AdminApi";
@ -40,6 +46,8 @@ import {ZoneSocket} from "../RoomManager";
import {Zone} from "_Model/Zone";
import Debug from "debug";
import {Admin} from "_Model/Admin";
import crypto from "crypto";
const debug = Debug('sockermanager');
@ -275,6 +283,12 @@ export class SocketManager {
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password);
}
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
@ -295,6 +309,12 @@ export class SocketManager {
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password);
}
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
@ -487,6 +507,11 @@ export class SocketManager {
webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setName(otherUser.name);
webrtcStartMessage1.setInitiator(true);
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET);
webrtcStartMessage1.setWebrtcusername(username);
webrtcStartMessage1.setWebrtcpassword(password);
}
const serverToClientMessage1 = new ServerToClientMessage();
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
@ -500,6 +525,11 @@ export class SocketManager {
webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setName(user.name);
webrtcStartMessage2.setInitiator(false);
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
webrtcStartMessage2.setWebrtcusername(username);
webrtcStartMessage2.setWebrtcpassword(password);
}
const serverToClientMessage2 = new ServerToClientMessage();
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
@ -512,6 +542,25 @@ export class SocketManager {
}
}
/**
* Computes a unique user/password for the TURN server, using a shared secret between the WorkAdventure API server
* and the Coturn server.
* The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
*/
private getTURNCredentials(name: string, secret: string): {username: string, password: string} {
const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours
const username = [unixTimeStamp, name].join(':');
const hmac = crypto.createHmac('sha1', secret);
hmac.setEncoding('base64');
hmac.write(username);
hmac.end();
const password = hmac.read();
return {
username: username,
password: password
};
}
//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

View File

@ -0,0 +1,20 @@
# The base domain
DOMAIN=workadventure.localhost
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=
# URL of the TURN server (needed to "punch a hole" through some networks for P2P connections)
TURN_SERVER=
TURN_USER=
TURN_PASSWORD=
# The URL used by default, in the form: "/_/global/map/url.json"
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json
# The email address used by Let's encrypt to send renewal warnings (compulsory)
ACME_EMAIL=

View File

@ -0,0 +1,100 @@
version: "3.3"
services:
reverse-proxy:
image: traefik:v2.3
command:
- --log.level=WARN
#- --api.insecure=true
- --providers.docker
- --entryPoints.web.address=:80
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entryPoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.email=d.negrier@thecodingmachine.com
- --certificatesresolvers.myresolver.acme.storage=/acme.json
# used during the challenge
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
# The Web UI (enabled by --api.insecure=true)
#- "8080:8080"
depends_on:
- pusher
- front
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./acme.json:/acme.json
restart: unless-stopped
front:
build:
context: ../..
dockerfile: front/Dockerfile
#image: thecodingmachine/workadventure-front:master
environment:
DEBUG_MODE: "$DEBUG_MODE"
JITSI_URL: $JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
API_URL: pusher.${DOMAIN}
TURN_SERVER: "${TURN_SERVER}"
TURN_USER: "${TURN_USER}"
TURN_PASSWORD: "${TURN_PASSWORD}"
START_ROOM_URL: "${START_ROOM_URL}"
labels:
- "traefik.http.routers.front.rule=Host(`play.${DOMAIN}`)"
- "traefik.http.routers.front.entryPoints=web,traefik"
- "traefik.http.services.front.loadbalancer.server.port=80"
- "traefik.http.routers.front-ssl.rule=Host(`play.${DOMAIN}`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.service=front"
- "traefik.http.routers.front-ssl.tls.certresolver=myresolver"
restart: unless-stopped
pusher:
build:
context: ../..
dockerfile: pusher/Dockerfile
#image: thecodingmachine/workadventure-pusher:master
command: yarn run runprod
environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey
API_URL: back:50051
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
labels:
- "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)"
- "traefik.http.routers.pusher.entryPoints=web,traefik"
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
- "traefik.http.routers.pusher-ssl.rule=Host(`pusher.${DOMAIN}`)"
- "traefik.http.routers.pusher-ssl.entryPoints=websecure"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.service=pusher"
- "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver"
restart: unless-stopped
back:
build:
context: ../..
dockerfile: back/Dockerfile
#image: thecodingmachine/workadventure-back:master
command: yarn run runprod
environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
ADMIN_API_URL: "$ADMIN_API_URL"
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
labels:
- "traefik.http.routers.back.rule=Host(`api.${DOMAIN}`)"
- "traefik.http.routers.back.entryPoints=web"
- "traefik.http.services.back.loadbalancer.server.port=8080"
- "traefik.http.routers.back-ssl.rule=Host(`api.${DOMAIN}`)"
- "traefik.http.routers.back-ssl.entryPoints=websecure"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.service=back"
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
restart: unless-stopped

View File

@ -3,7 +3,8 @@
local namespace = env.GITHUB_REF_SLUG,
local tag = namespace,
local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com",
local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://"+url else null,
// develop branch does not use admin because of issue with SSL certificate of admin as of now.
local adminUrl = if namespace == "master" /*|| namespace == "develop"*/ || std.startsWith(namespace, "admin") then "https://"+url else null,
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
"version": "1.0",
"containers": {
@ -21,6 +22,7 @@
"JITSI_ISS": env.JITSI_ISS,
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
} + if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}
@ -39,6 +41,7 @@
"JITSI_ISS": env.JITSI_ISS,
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
} + if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}
@ -79,6 +82,7 @@
"TURN_USER": "workadventure",
"TURN_PASSWORD": "WorkAdventure123",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
"START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json"
//"GA_TRACKING_ID": "UA-10196481-11"
}
},

View File

@ -1,20 +0,0 @@
version: '3'
services:
wait_app:
image: dadarek/wait-for-dependencies
depends_on:
- reverse-proxy
command: front:8080
cypress:
# the Docker image to use from https://github.com/cypress-io/cypress-docker-images
image: "cypress/included:3.8.3"
depends_on:
- reverse-proxy
environment:
# pass base url to test pointing at the web application
- CYPRESS_baseUrl=http://front:8080
working_dir: /e2e
volumes:
- ./e2e/:/e2e

View File

@ -31,9 +31,12 @@ services:
ADMIN_URL: workadventure.localhost
STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
TURN_USER: workadventure
TURN_PASSWORD: WorkAdventure123
STUN_SERVER: "stun:stun.l.google.com:19302"
TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349"
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
TURN_USER: ""
TURN_PASSWORD: ""
START_ROOM_URL: "$START_ROOM_URL"
command: yarn run start
volumes:
@ -108,6 +111,7 @@ services:
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
volumes:
- ./back:/usr/src/app
labels:
@ -149,3 +153,28 @@ services:
- ./back:/usr/src/back
- ./front:/usr/src/front
- ./pusher:/usr/src/pusher
# coturn:
# image: coturn/coturn:4.5.2
# command:
# - turnserver
# #- -c=/etc/coturn/turnserver.conf
# - --log-file=stdout
# - --external-ip=$$(detect-external-ip)
# - --listening-port=3478
# - --min-port=10000
# - --max-port=10010
# - --tls-listening-port=5349
# - --listening-ip=0.0.0.0
# - --realm=coturn.workadventure.localhost
# - --server-name=coturn.workadventure.localhost
# - --lt-cred-mech
# # Enable Coturn "REST API" to validate temporary passwords.
# #- --use-auth-secret
# #- --static-auth-secret=SomeStaticAuthSecret
# #- --userdb=/var/lib/turn/turndb
# - --user=workadventure:WorkAdventure123
# # use real-valid certificate/privatekey files
# #- --cert=/root/letsencrypt/fullchain.pem
# #- --pkey=/root/letsencrypt/privkey.pem
# network_mode: host

3
e2e/.gitignore vendored
View File

@ -1,3 +0,0 @@
screenshots/
videos/
node_modules/

View File

@ -1,36 +0,0 @@
# Testing with cypress
This project use [cypress](https://www.cypress.io/) to do functional testing of the website.
Unfortunately we cannot integrate it with docker-compose for the moment, so you will need to install some packages locally on your pc.
## Getting Started
You will need to install theses dependancies on linux (don't know about mac):
```bash
sudo apt update
sudo apt install libgtk2.0-0 libgtk-3-0 libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
```
Cypress can be installed locally in the e2e directory
```bash
cd e2e
npm install
```
How to use:
```bash
npm run cy:run
npm run cy:open
```
The first command will run all tests in the terminal, while the second will open the interactive task runner which allow you to easily manage the test workflow
[More details here](https://docs.cypress.io/guides/getting-started/testing-your-app.html#Step-1-Start-your-server)
## How to test a game
Cypress cannot "see" and so cannot directly manipulate the canva created by Phaser.
This means we have to do workarounds such as exposing core objects in the window so that cypress can manipulate them or doing console that cypress can catch.

View File

@ -1,7 +0,0 @@
{
"baseUrl": "http://workadventure.localhost",
"video": false,
"defaultCommandTimeout": 20000,
"pluginsFile": false,
"supportFile": false
}

View File

@ -1,25 +0,0 @@
Cypress.on('window:before:load', (win) => {
// because this is called before any scripts
// have loaded - the ga function is undefined
// so we need to create it.
win.cypressAsserter = cy.stub().as('ca')
})
describe('WorkAdventureGame', () => {
beforeEach(() => {
cy.visit('/', {
onBeforeLoad (win) {
cy.spy(win.console, 'log').as('console.log')
},
})
});
it('loads', () => {
cy.get('@console.log').should('be.calledWith', 'Started the game')
cy.get('@console.log').should('be.calledWith', 'Preloading')
cy.get('@console.log').should('be.calledWith', 'Preloading done')
cy.get('@console.log').should('be.calledWith', 'startInit')
cy.get('@console.log').should('be.calledWith', 'startInit done')
});
});

1406
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
{
"dependencies": {
"cypress": "^3.8.3"
},
"scripts": {
"cy:run": "cypress run",
"cy:open": "cypress open"
}
}

1
front/dist/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
index.html

View File

@ -15,6 +15,14 @@
#gameMenu section {
margin: 10px;
}
section#socialLinks{
position: absolute;
margin-bottom: 0;
}
section#socialLinks img{
width: 32px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
</style>
<div id="gameMenu" hidden>
@ -38,5 +46,9 @@
<section id="adminConsoleSection" hidden>
<button id="adminConsoleButton">Admin console</button>
</section>
<section id="socialLinks" hidden>
<a class="not-button" href="https://www.facebook.com/workadventurebytcm" target="_blank"><img class="not-button" src="/resources/objects/facebook-icon.png"/></a>
<a class="not-button" href="https://twitter.com/Workadventure_" target="_blank"><img class="not-button" src="/resources/objects/twitter-icon.png"/></a>
</section>
</main>
</div>

View File

@ -0,0 +1,122 @@
<style>
*{
font-family: 'Open Sans', sans-serif;
cursor: url('/resources/logos/cursor_normal.png'), auto;
}
* a, button, input{
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
#gameReport {
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
margin: 2px auto 0;
width: 298px;
}
#gameReport h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#gameReport h3 {
margin: 0;
}
#gameReport textarea {
font-size: 70%;
background: linear-gradient(top, #d6d7d7, #dee0e0);
border: 1px solid #a1a3a3;
border-radius: 4px;
box-shadow: 0 1px #fff;
box-sizing: border-box;
color: #696969;
height: 100px;
transition: box-shadow 0.3s;
width: 100%;
}
#gameReport section {
margin: 10px;
}
#gameReport section.action{
text-align: center;
margin: 0;
}
#gameReport button {
margin-top: 10px;
font-size: 60%;
background-color: #dc3545;
color: white;
border-radius: 7px;
padding: 3px 10px 3px 10px;
}
#gameReport button#gameReportFormCancel {
background-color: #c7c7c700;
color: #292929;
display: block;
float: right;
}
#gameReport section a{
text-align: center;
font-size: 12px;
margin: 0 6px;
color: black;
}
#gameReport section h6,
#gameReport section h5{
margin: 1px;
}
#gameReport section.text-center{
text-align: center;
}
#gameReport p{
font-size: 8px;
margin: 3px 0 0 0;
}
#gameReport form p{
margin: 0px 70px;
}
#gameReport section p.err{
color: red;
display: none;
}
#gameReport section p.info{
display: none;
}
</style>
<main id="gameReport" hidden>
<section>
<button id="gameReportFormCancel">X</button>
<h1>Moderate <span id="nameReported"></span></h1>
<p id="askActionP">What action do you want to take?</p>
</section>
<section>
<h3>Block: </h3>
<p>Block any communication from and to this user. This can be reverted.</p>
<section class="action">
<button id="toggleBlockButton">Block this user</button>
</section>
</section>
<section id="reportSection">
<h3>Report: </h3>
<p>Send a report message to the administrators of this room. They may later ban this user.</p>
<form>
<section>
<h6>Your message: </h6>
<textarea type="text" name="report" id="gameReportInput"></textarea>
<p class="err" id="gameReportErr"></p>
</section>
<section class="action">
<button type="submit" id="gameReportFormSubmit">Report this user</button>
</section>
</form>
</section>
</main>

View File

@ -14,9 +14,6 @@
width: 298px;
height: 150px;
}
#gameShare .cautiousText {
font-size: 50%;
}
#gameShare h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;

View File

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

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
front/dist/resources/logos/cancel.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
front/dist/resources/logos/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56.48 56.48"><defs><style>.cls-1{fill:#e76e54;}.cls-2{fill:#fff;}</style></defs><path class="cls-1" d="M39.94,512H16.54L0,495.46v-23.4l16.54-16.54h23.4l16.54,16.54v23.4Z" transform="translate(0 -455.52)"/><path class="cls-2" d="M33.54,485.52H23l-1.77-21.18H35.3Z" transform="translate(0 -455.52)"/><path class="cls-2" d="M23,492.58H33.54v10.59H23Z" transform="translate(0 -455.52)"/></svg>

After

Width:  |  Height:  |  Size: 477 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -39,6 +39,7 @@ body .message-info.warning{
position: relative;
transition: all 0.2s ease;
background-color: #00000099;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
.video-container i{
position: absolute;
@ -53,25 +54,71 @@ body .message-info.warning{
font-size: 28px;
color: white;
}
.video-container img.active{
display: block;
}
.video-container img{
position: absolute;
display: none;
width: 15px;
height: 15px;
background: #d93025;
border-radius: 48px;
width: 25px;
height: 25px;
left: 5px;
bottom: 5px;
padding: 10px;
z-index: 2;
}
.video-container img.block-logo {
left: 30%;
bottom: 15%;
width: 150px;
height: 150px;
}
.video-container img.report{
.video-container button.report{
display: block;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
background: none;
background-color: rgba(0, 0, 0, 0);
border: none;
background-color: black;
border-radius: 15px;
position: absolute;
width: 0px;
height: 35px;
right: 5px;
left: auto;
bottom: 5px;
padding: 0px;
overflow: hidden;
z-index: 2;
transition: all .5s ease;
}
.video-container:hover button.report{
width: 35px;
padding: 10px;
}
.video-container button.report:hover {
width: 150px;
}
.video-container button.report img{
position: absolute;
display: block;
bottom: 5px;
left: 5px;
margin: 0;
padding: 0;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
.video-container button.report span{
position: absolute;
bottom: 8px;
left: 36px;
color: white;
font-size: 16px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
.video-container img.active {
display: block !important;
}
.video-container video{
@ -150,10 +197,7 @@ video#myCamVideo{
transition: all .2s;
right: 224px;
}
/*.btn-call{
transition: all .1s;
left: 0px;
}*/
.btn-cam-action div img{
height: 22px;
width: 30px;
@ -352,6 +396,7 @@ body {
#cowebsite {
position: fixed;
transition: transform 0.5s;
background-color: white;
}
#cowebsite.loading {
background-color: gray;
@ -1078,7 +1123,7 @@ div.modal-report-user{
white-space: pre-wrap;
word-wrap: break-word;
}
.discussion .messages .message p.a{
.discussion .messages .message p a{
color: white;
}

View File

@ -26,9 +26,10 @@
"axios": "^0.21.1",
"generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0",
"phaser": "^3.52.0",
"phaser": "3.24.1",
"queue-typescript": "^1.0.1",
"quill": "^1.3.7",
"rxjs": "^6.6.3",
"simple-peer": "^9.6.2",
"socket.io-client": "^2.3.0",
"webpack-require-http": "^0.4.3"

View File

@ -332,7 +332,7 @@ export class ConsoleGlobalMessageManager {
}
active(){
this.userInputManager.clearAllInputKeyboard();
this.userInputManager.clearAllKeys();
this.divMainConsole.style.top = '0';
this.activeConsole = true;
}

View File

@ -1,5 +1,8 @@
import {RoomConnection} from "../Connexion/RoomConnection";
import * as TypeMessages from "./TypeMessage";
import List = Phaser.Structs.List;
import {UpdatedLocalStreamCallback} from "../WebRtc/MediaManager";
import {Banned} from "./TypeMessage";
export interface TypeMessageInterface {
showMessage(message: string): void;
@ -8,6 +11,7 @@ export interface TypeMessageInterface {
export class UserMessageManager {
typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
receiveBannedMessageListener: Set<Function> = new Set<UpdatedLocalStreamCallback>();
constructor(private Connection: RoomConnection) {
const valueTypeMessageTab = Object.values(TypeMessages);
@ -21,7 +25,14 @@ export class UserMessageManager {
initialise() {
//receive signal to show message
this.Connection.receiveUserMessage((type: string, message: string) => {
this.showMessage(type, message);
const typeMessage = this.showMessage(type, message);
//listener on banned receive message
if(typeMessage instanceof Banned) {
for (const callback of this.receiveBannedMessageListener) {
callback();
}
}
});
}
@ -32,5 +43,10 @@ export class UserMessageManager {
return;
}
classTypeMessage.showMessage(message);
return classTypeMessage;
}
setReceiveBanListener(callback: Function){
this.receiveBannedMessageListener.add(callback);
}
}

View File

@ -96,7 +96,9 @@ export interface WebRtcSignalSentMessageInterface {
export interface WebRtcSignalReceivedMessageInterface {
userId: number,
signal: SignalData
signal: SignalData,
webRtcUser: string | undefined,
webRtcPassword: string | undefined
}
export interface StartMapInterface {

View File

@ -427,7 +427,9 @@ export class RoomConnection implements RoomConnection {
callback({
userId: message.getUserid(),
name: message.getName(),
initiator: message.getInitiator()
initiator: message.getInitiator(),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
});
});
}
@ -436,7 +438,9 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_SIGNAL, (message: WebRtcSignalToClientMessage) => {
callback({
userId: message.getUserid(),
signal: JSON.parse(message.getSignal())
signal: JSON.parse(message.getSignal()),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
});
});
}
@ -445,7 +449,9 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, (message: WebRtcSignalToClientMessage) => {
callback({
userId: message.getUserid(),
signal: JSON.parse(message.getSignal())
signal: JSON.parse(message.getSignal()),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
});
});
}
@ -464,7 +470,8 @@ export class RoomConnection implements RoomConnection {
});
}
public getUserId(): number|null {
public getUserId(): number {
if (this.userId === null) throw 'UserId cannot be null!'
return this.userId;
}
@ -583,7 +590,7 @@ export class RoomConnection implements RoomConnection {
public hasTag(tag: string): boolean {
return this.tags.includes(tag);
}
public isAdmin(): boolean {
return this.hasTag('admin');
}

View File

@ -1,36 +0,0 @@
declare let window:WindowWithCypressAsserter;
interface WindowWithCypressAsserter extends Window {
cypressAsserter: CypressAsserter;
}
//this class is used to communicate with cypress, our e2e testing client
//Since cypress cannot manipulate canvas, we notified it with console logs
class CypressAsserter {
constructor() {
window.cypressAsserter = this
}
gameStarted() {
console.log('Started the game')
}
preloadStarted() {
console.log('Preloading')
}
preloadFinished() {
console.log('Preloading done')
}
initStarted() {
console.log('startInit')
}
initFinished() {
console.log('startInit done')
}
}
export const cypressAsserter = new CypressAsserter()

View File

@ -3,9 +3,10 @@ const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.wo
const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "pusher.workadventure.localhost");
const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.workadventure.localhost');
const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "workadventure.localhost");
const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca";
const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$';
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || "";
const TURN_USER: string = process.env.TURN_USER || '';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || '';
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
const RESOLUTION = 2;
@ -23,6 +24,7 @@ export {
ZOOM_LEVEL,
POSITION_DELAY,
MAX_EXTRAPOLATION_TIME,
STUN_SERVER,
TURN_SERVER,
TURN_USER,
TURN_PASSWORD,

View File

@ -1,14 +1,54 @@
import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig;
export const addLoader = (scene:Phaser.Scene): void => {
const loadingText = scene.add.text(scene.game.renderer.width / 2, 200, 'Loading');
const LogoNameIndex: string = 'logoLoading';
const TextName: string = 'Loading...';
const LogoResource: string = 'resources/logos/logo.png';
const LogoFrame: ImageFrameConfig = {frameWidth: 307, frameHeight: 59};
export const addLoader = (scene: Phaser.Scene): void => {
// If there is nothing to load, do not display the loader.
if (scene.load.list.entries.length === 0) {
return;
}
let loadingText: Phaser.GameObjects.Text|null = null;
const loadingBarWidth: number = Math.floor(scene.game.renderer.width / 3);
const loadingBarHeight: number = 16;
const padding: number = 5;
const promiseLoadLogoTexture = new Promise<Phaser.GameObjects.Image>((res) => {
if(scene.load.textureManager.exists(LogoNameIndex)){
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex));
}else{
//add loading if logo image is not ready
loadingText = scene.add.text(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 50, TextName);
}
scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame);
scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => {
if(loadingText){
loadingText.destroy();
}
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex));
});
});
const progressContainer = scene.add.graphics();
const progress = scene.add.graphics();
progressContainer.fillStyle(0x444444, 0.8);
progressContainer.fillRect((scene.game.renderer.width - loadingBarWidth) / 2 - padding, scene.game.renderer.height / 2 + 50 - padding, loadingBarWidth + padding * 2, loadingBarHeight + padding * 2);
scene.load.on('progress', (value: number) => {
progress.clear();
progress.fillStyle(0xffffff, 1);
progress.fillRect(0, 270, 800 * value, 60);
progress.fillStyle(0xBBBBBB, 1);
progress.fillRect((scene.game.renderer.width - loadingBarWidth) / 2, scene.game.renderer.height / 2 + 50, loadingBarWidth * value, loadingBarHeight);
});
scene.load.on('complete', () => {
loadingText.destroy();
if(loadingText){
loadingText.destroy();
}
promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => {
resLoadingImage.destroy();
});
progress.destroy();
progressContainer.destroy();
});
}
}

View File

@ -3,14 +3,12 @@ import {discussionManager} from "../../WebRtc/DiscussionManager";
export const openChatIconName = 'openChatIcon';
export class OpenChatIcon extends Phaser.GameObjects.Image {
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, openChatIconName);
super(scene, x, y, openChatIconName, 3);
scene.add.existing(this);
this.setScrollFactor(0, 0);
this.setOrigin(0, 1);
this.displayWidth = 30;
this.displayHeight = 30;
this.setInteractive();
this.setVisible(false)
this.setVisible(false);
this.setDepth(99999);
this.on("pointerup", () => discussionManager.showDiscussionPart());

View File

@ -6,7 +6,8 @@ export interface BodyResourceDescriptionListInterface {
export interface BodyResourceDescriptionInterface {
name: string,
img: string
img: string,
level?: number
}
export const PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {

View File

@ -23,21 +23,26 @@ export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptio
});
return returnArray;
}
export const loadCustomTexture = (load: LoaderPlugin, texture: CharacterTexture) => {
export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise<BodyResourceDescriptionInterface> => {
const name = 'customCharacterTexture'+texture.id;
load.spritesheet(name,texture.url,{frameWidth: 32, frameHeight: 32});
return name;
const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level}
return createLoadingPromise(loaderPlugin, playerResourceDescriptor);
}
export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturePlugin: TextureManager, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => {
const promisesList:Promise<void>[] = [];
export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => {
const promisesList:Promise<unknown>[] = [];
texturekeys.forEach((textureKey: string|BodyResourceDescriptionInterface) => {
const playerResourceDescriptor = getRessourceDescriptor(textureKey);
if(!texturePlugin.exists(playerResourceDescriptor.name)) {
console.log('Loading '+playerResourceDescriptor.name)
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor));
try {
//TODO refactor
const playerResourceDescriptor = getRessourceDescriptor(textureKey);
if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor));
}
}catch (err){
console.error(err);
}
})
});
let returnPromise:Promise<Array<string|BodyResourceDescriptionInterface>>;
if (promisesList.length > 0) {
loadPlugin.start();
@ -66,8 +71,14 @@ export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptio
}
const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) => {
return new Promise<void>((res, rej) => {
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, {frameWidth: 32, frameHeight: 32});
loadPlugin.once('filecomplete-spritesheet-'+playerResourceDescriptor.name, () => res());
return new Promise<BodyResourceDescriptionInterface>((res) => {
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
return res(playerResourceDescriptor);
}
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, {
frameWidth: 32,
frameHeight: 32
});
loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor));
});
}

View File

@ -1,7 +1,6 @@
import {GameScene} from "../Game/GameScene";
import {PointInterface} from "../../Connexion/ConnexionModels";
import {Character} from "../Entity/Character";
import {Sprite} from "./Sprite";
/**
* Class representing the sprite of a remote player (a player that plays on another computer)
@ -23,6 +22,11 @@ export class RemotePlayer extends Character {
//set data
this.userId = userId;
//todo: implement on click action
/*this.playerName.setInteractive();
this.playerName.on('pointerup', () => {
});*/
}
updatePosition(position: PointInterface): void {

View File

@ -14,7 +14,7 @@ export class SpeechBubble {
const bubbleWidth = bubblePadding * 2 + text.length * 10;
const arrowHeight = bubbleHeight / 4;
this.bubble = scene.add.graphics({ x: player.x + 16, y: player.y - 80 });
this.bubble = scene.add.graphics({ x: 16, y: -80 });
player.add(this.bubble);
// Bubble shadow

View File

@ -49,6 +49,10 @@ export class GameMap {
this.lastProperties = newProps;
}
public getCurrentProperties(): Map<string, string|boolean|number> {
return this.lastProperties;
}
private getProperties(key: number): Map<string, string|boolean|number> {
const properties = new Map<string, string|boolean|number>();

View File

@ -30,10 +30,11 @@ import {RemotePlayer} from "../Entity/RemotePlayer";
import {Queue} from 'queue-typescript';
import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
import {lazyLoadPlayerCharacterTextures} from "../Entity/PlayerTexturesLoadingManager";
import {lazyLoadPlayerCharacterTextures, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager";
import {
CenterListener,
EXIT_MESSAGE_PROPERTIES, JITSI_MESSAGE_PROPERTIES,
EXIT_MESSAGE_PROPERTIES,
JITSI_MESSAGE_PROPERTIES,
layoutManager,
LayoutMode,
ON_ACTION_TRIGGER_BUTTON,
@ -72,6 +73,8 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha
import {TextureError} from "../../Exception/TextureError";
import {addLoader} from "../Components/Loader";
import {ErrorSceneName} from "../Reconnecting/ErrorScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
export interface GameSceneInitInterface {
initPosition: PointInterface|null,
@ -116,7 +119,7 @@ export class GameScene extends ResizableScene implements CenterListener {
MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey : Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
Map!: Phaser.Tilemaps.Tilemap;
Layers!: Array<Phaser.Tilemaps.TilemapLayer>;
Layers!: Array<Phaser.Tilemaps.StaticTilemapLayer>;
Objects!: Array<Phaser.Physics.Arcade.Sprite>;
mapFile!: ITiledMap;
groups: Map<number, Sprite>;
@ -157,7 +160,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private actionableItems: Map<number, ActionableItem> = new Map<number, ActionableItem>();
// The item that can be selected by pressing the space key.
private outlinedItem: ActionableItem|null = null;
private userInputManager!: UserInputManager;
public userInputManager!: UserInputManager;
private isReconnecting: boolean = false;
private startLayerName!: string | null;
private openChatIcon!: OpenChatIcon;
@ -186,7 +189,13 @@ export class GameScene extends ResizableScene implements CenterListener {
//hook preload scene
preload(): void {
addLoader(this);
const localUser = localUserStore.getLocalUser();
const textures = localUser?.textures;
if (textures) {
for (const texture of textures) {
loadCustomTexture(this.load, texture);
}
}
this.load.image(openChatIconName, 'resources/objects/talk.png');
this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => {
@ -210,6 +219,8 @@ export class GameScene extends ResizableScene implements CenterListener {
this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32});
this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
addLoader(this);
}
// FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving.
@ -344,11 +355,11 @@ export class GameScene extends ResizableScene implements CenterListener {
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
//add layer on map
this.Layers = new Array<Phaser.Tilemaps.TilemapLayer>();
this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>();
let depth = -2;
for (const layer of this.mapFile.layers) {
if (layer.type === 'tilelayer') {
this.addLayer(this.Map.createLayer(layer.name, this.Terrains, 0, 0).setDepth(depth));
this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth));
const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl !== undefined) {
@ -529,6 +540,7 @@ export class GameScene extends ResizableScene implements CenterListener {
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
this.UserMessageManager = new UserMessageManager(this.connection);
this.UserMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
const self = this;
this.simplePeer.registerPeerConnectionListener({
@ -625,6 +637,15 @@ export class GameScene extends ResizableScene implements CenterListener {
}
}
private safeParseJSONstring(jsonString: string|undefined, propertyName: string) {
try {
return jsonString ? JSON.parse(jsonString) : {};
} catch(e) {
console.warn('Invalid JSON found in property "' + propertyName + '" of the map:' + jsonString, e);
return {}
}
}
private triggerOnMapLayerPropertyChange(){
/* @deprecated
this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => {
@ -656,7 +677,7 @@ export class GameScene extends ResizableScene implements CenterListener {
coWebsiteManager.closeCoWebsite();
}else{
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(newValue as string);
coWebsiteManager.loadCoWebsite(newValue as string, allProps.get('openWebsitePolicy') as string | undefined);
layoutManager.removeActionButton('openWebsite', this.userInputManager);
};
@ -680,11 +701,13 @@ export class GameScene extends ResizableScene implements CenterListener {
this.stopJitsi();
}else{
const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance);
if (JITSI_PRIVATE_MODE) {
const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined;
this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag);
this.connection.emitQueryJitsiJwtMessage(roomName, adminTag);
} else {
this.startJitsi(newValue as string);
this.startJitsi(roomName, undefined);
}
layoutManager.removeActionButton('jitsiRoom', this.userInputManager);
}
@ -725,6 +748,10 @@ export class GameScene extends ResizableScene implements CenterListener {
if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey);
urlManager.pushStartLayerNameToUrl(hash);
if (roomId !== this.scene.key) {
if (this.scene.get(roomId) === null) {
console.error("next room not loaded", exitKey);
return;
}
this.cleanupClosingScene();
this.scene.stop();
this.scene.remove(this.scene.key);
@ -878,13 +905,13 @@ export class GameScene extends ResizableScene implements CenterListener {
this.cameras.main.setZoom(ZOOM_LEVEL);
}
addLayer(Layer : Phaser.Tilemaps.TilemapLayer){
addLayer(Layer : Phaser.Tilemaps.StaticTilemapLayer){
this.Layers.push(Layer);
}
createCollisionWithPlayer() {
//add collision layer
this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => {
this.Layers.forEach((Layer: Phaser.Tilemaps.StaticTilemapLayer) => {
this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => {
//this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name)
});
@ -902,7 +929,7 @@ export class GameScene extends ResizableScene implements CenterListener {
createCurrentPlayer(){
//TODO create animation moving between exit and start
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.textures, this.characterLayers);
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers);
try {
this.CurrentPlayer = new Player(
this,
@ -1096,7 +1123,7 @@ export class GameScene extends ResizableScene implements CenterListener {
return;
}
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.textures, addPlayerData.characterLayers);
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, addPlayerData.characterLayers);
const player = new RemotePlayer(
addPlayerData.userId,
this,
@ -1221,6 +1248,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private reposition(): void {
this.presentationModeSprite.setY(this.game.renderer.height - 2);
this.chatModeSprite.setY(this.game.renderer.height - 2);
this.openChatIcon.setY(this.game.renderer.height - 2);
// Recompute camera offset if needed
this.updateCameraOffset();
@ -1247,7 +1275,11 @@ export class GameScene extends ResizableScene implements CenterListener {
}
public startJitsi(roomName: string, jwt?: string): void {
jitsiFactory.start(roomName, this.playerName, jwt);
const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string|undefined, 'jitsiConfig');
const jitsiInterfaceConfig = this.safeParseJSONstring(allProps.get("jitsiInterfaceConfig") as string|undefined, 'jitsiInterfaceConfig');
jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig);
this.connection.setSilent(true);
mediaManager.hideGameOverlay();
@ -1265,5 +1297,14 @@ export class GameScene extends ResizableScene implements CenterListener {
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
}
private bannedUser(){
this.cleanupClosingScene();
this.userInputManager.clearAllKeys();
this.scene.start(ErrorSceneName, {
title: 'Banned',
subTitle: 'You was banned of WorkAdventure',
message: 'If you want more information, you can contact us: workadventure@thecodingmachine.com'
});
}
}

View File

@ -43,8 +43,9 @@ export class ActionableItem {
}
this.isSelectable = true;
if (this.sprite.pipeline) {
this.sprite.setPipeline(OutlinePipeline.KEY);
this.sprite.pipeline.set2f('uTextureSize', this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height);
// Commented out to try to fix MacOS issue
/*this.sprite.setPipeline(OutlinePipeline.KEY);
this.sprite.pipeline.set2f('uTextureSize', this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height);*/
}
}
@ -56,7 +57,8 @@ export class ActionableItem {
return;
}
this.isSelectable = false;
this.sprite.resetPipeline();
// Commented out to try to fix MacOS issue
//this.sprite.resetPipeline();
}
/**

View File

@ -0,0 +1,41 @@
import {ResizableScene} from "./ResizableScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager";
import {CharacterTexture} from "../../Connexion/LocalUser";
export abstract class AbstractCharacterScene extends ResizableScene {
loadCustomSceneSelectCharacters() : Promise<BodyResourceDescriptionInterface[]> {
const textures = this.getTextures();
const promises : Promise<BodyResourceDescriptionInterface>[] = [];
if (textures) {
for (const texture of textures) {
if (texture.level === -1) {
continue;
}
promises.push(loadCustomTexture(this.load, texture));
}
}
return Promise.all(promises)
}
loadSelectSceneCharacters() : Promise<BodyResourceDescriptionInterface[]> {
const textures = this.getTextures();
const promises: Promise<BodyResourceDescriptionInterface>[] = [];
if (textures) {
for (const texture of textures) {
if (texture.level !== -1) {
continue;
}
promises.push(loadCustomTexture(this.load, texture));
}
}
return Promise.all(promises)
}
private getTextures() : CharacterTexture[]|undefined{
const localUser = localUserStore.getLocalUser();
return localUser?.textures;
}
}

View File

@ -10,6 +10,7 @@ import {ResizableScene} from "./ResizableScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {addLoader} from "../Components/Loader";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene";
export const CustomizeSceneName = "CustomizeScene";
@ -20,7 +21,7 @@ enum CustomizeTextures{
arrowUp = "arrow_up",
}
export class CustomizeScene extends ResizableScene {
export class CustomizeScene extends AbstractCharacterScene {
private textField!: TextField;
private enterField!: TextField;
@ -48,29 +49,21 @@ export class CustomizeScene extends ResizableScene {
preload() {
addLoader(this);
this.layers = loadAllLayers(this.load);
this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
if(!bodyResourceDescription.level){
throw 'Texture level is null';
}
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
});
});
this.load.image(CustomizeTextures.arrowRight, "resources/objects/arrow_right.png");
this.load.image(CustomizeTextures.icon, "resources/logos/tcm_full.png");
this.load.image(CustomizeTextures.arrowUp, "resources/objects/arrow_up.png");
this.load.bitmapFont(CustomizeTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.layers = loadAllLayers(this.load);
const localUser = localUserStore.getLocalUser();
const textures = localUser?.textures;
if (textures) {
for (const texture of textures) {
if(texture.level === -1){
continue;
}
loadCustomTexture(this.load, texture);
const name = 'customCharacterTexture'+texture.id;
this.layers[texture.level].unshift({
name,
img: texture.url
});
}
}
}
create() {

View File

@ -2,7 +2,6 @@ import {gameManager} from "../Game/GameManager";
import {TextField} from "../Components/TextField";
import {TextInput} from "../Components/TextInput";
import Image = Phaser.GameObjects.Image;
import {cypressAsserter} from "../../Cypress/CypressAsserter";
import {SelectCharacterSceneName} from "./SelectCharacterScene";
import {ResizableScene} from "./ResizableScene";
@ -29,16 +28,13 @@ export class LoginScene extends ResizableScene {
}
preload() {
cypressAsserter.preloadStarted();
//this.load.image(LoginTextures.playButton, "resources/objects/play_button.png");
this.load.image(LoginTextures.icon, "resources/logos/tcm_full.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
cypressAsserter.preloadFinished();
}
create() {
cypressAsserter.initStarted();
this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Enter your name:');
this.nameInput = new TextInput(this, this.game.renderer.width / 2, 70, 8, this.name,(text: string) => {
@ -59,8 +55,6 @@ export class LoginScene extends ResizableScene {
}
this.login(this.name);
});
cypressAsserter.initFinished();
}
update(time: number, delta: number): void {

View File

@ -9,6 +9,7 @@ import {localUserStore} from "../../Connexion/LocalUserStore";
import {loadAllDefaultModels, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager";
import {addLoader} from "../Components/Loader";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene";
//todo: put this constants in a dedicated file
@ -21,7 +22,7 @@ enum LoginTextures {
customizeButtonSelected = "customize_button_selected"
}
export class SelectCharacterScene extends ResizableScene {
export class SelectCharacterScene extends AbstractCharacterScene {
private readonly nbCharactersPerRow = 6;
private textField!: TextField;
private pressReturnField!: TextField;
@ -44,6 +45,13 @@ export class SelectCharacterScene extends ResizableScene {
preload() {
addLoader(this);
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription);
});
})
this.load.image(LoginTextures.playButton, "resources/objects/play_button.png");
this.load.image(LoginTextures.icon, "resources/logos/tcm_full.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
@ -52,17 +60,7 @@ export class SelectCharacterScene extends ResizableScene {
this.load.image(LoginTextures.customizeButton, 'resources/objects/customize.png');
this.load.image(LoginTextures.customizeButtonSelected, 'resources/objects/customize_selected.png');
const localUser = localUserStore.getLocalUser();
const textures = localUser?.textures;
if (textures) {
for (const texture of textures) {
if(texture.level !== -1){
continue;
}
const name = loadCustomTexture(this.load, texture);
this.playerModels.push({name: name, img: texture.url});
}
}
addLoader(this);
}
create() {
@ -127,7 +125,7 @@ export class SelectCharacterScene extends ResizableScene {
/*create user*/
this.createCurrentPlayer();
const playerNumber = localUserStore.getPlayerCharacterIndex();
if (playerNumber && playerNumber !== -1) {
this.selectedRectangleXPos = playerNumber % this.nbCharactersPerRow;

View File

@ -3,7 +3,9 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha
import {gameManager} from "../Game/GameManager";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {mediaManager} from "../../WebRtc/MediaManager";
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager";
export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu';
@ -21,6 +23,7 @@ export class MenuScene extends Phaser.Scene {
private menuElement!: Phaser.GameObjects.DOMElement;
private gameQualityMenuElement!: Phaser.GameObjects.DOMElement;
private gameShareElement!: Phaser.GameObjects.DOMElement;
private gameReportElement!: ReportMenu;
private sideMenuOpened = false;
private settingsMenuOpened = false;
private gameShareOpened = false;
@ -40,20 +43,21 @@ export class MenuScene extends Phaser.Scene {
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html');
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html');
this.load.html(gameShare, 'resources/html/gameShare.html');
this.load.html(gameReportKey, gameReportRessource);
}
create() {
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
this.menuElement.setOrigin(0);
this.revealMenusAfterInit(this.menuElement, 'gameMenu');
MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu');
const middleX = (window.innerWidth / 3) - 298;
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
this.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality');
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality');
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
this.revealMenusAfterInit(this.gameShareElement, gameShare);
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
this.gameShareElement.addListener('click');
this.gameShareElement.on('click', (event:MouseEvent) => {
event.preventDefault();
@ -64,6 +68,12 @@ export class MenuScene extends Phaser.Scene {
}
});
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
mediaManager.setShowReportModalCallBacks((userId, userName) => {
this.closeAll();
this.gameReportElement.open(parseInt(userId), userName);
});
this.input.keyboard.on('keyup-TAB', () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
});
@ -77,7 +87,8 @@ export class MenuScene extends Phaser.Scene {
this.menuElement.on('click', this.onMenuClick.bind(this));
}
private revealMenusAfterInit(menuElement: Phaser.GameObjects.DOMElement, rootDomId: string) {
//todo put this method in a parent menuElement class
static revealMenusAfterInit(menuElement: Phaser.GameObjects.DOMElement, rootDomId: string) {
//Dom elements will appear inside the viewer screen when creating before being moved out of it, which create a flicker effect.
//To prevent this, we put a 'hidden' attribute on the root element, we remove it only after the init is done.
setTimeout(() => {
@ -98,6 +109,11 @@ export class MenuScene extends Phaser.Scene {
const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement;
adminSection.hidden = false;
}
//TODO bind with future metadata of card
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement;
adminSection.hidden = false;
//}
this.tweens.add({
targets: this.menuElement,
x: openedSideMenuX,
@ -222,6 +238,9 @@ export class MenuScene extends Phaser.Scene {
}
private onMenuClick(event:MouseEvent) {
if((event?.target as HTMLInputElement).classList.contains('not-button')){
return;
}
event.preventDefault();
switch ((event?.target as HTMLInputElement).id) {
@ -280,5 +299,6 @@ export class MenuScene extends Phaser.Scene {
private closeAll(){
this.closeGameQualityMenu();
this.closeGameShare();
this.gameReportElement.close();
}
}

View File

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

View File

@ -1,4 +1,4 @@
export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.MultiPipeline {
export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline {
// the unique id of this pipeline
public static readonly KEY = 'Outline';

View File

@ -59,7 +59,11 @@ export class UserInputManager {
];
}
clearAllInputKeyboard(){
clearAllListeners(){
this.Scene.input.keyboard.removeAllListeners();
}
clearAllKeys(){
this.Scene.input.keyboard.removeAllKeys();
}

View File

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

View File

@ -42,7 +42,7 @@ class CoWebsiteManager {
this.opened = iframeStates.opened;
}
public loadCoWebsite(url: string): void {
public loadCoWebsite(url: string, allowPolicy?: string): void {
this.load();
this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg">
@ -56,6 +56,9 @@ class CoWebsiteManager {
const iframe = document.createElement('iframe');
iframe.id = 'cowebsite-iframe';
iframe.src = url;
if (allowPolicy) {
iframe.allow = allowPolicy;
}
const onloadPromise = new Promise((resolve) => {
iframe.onload = () => resolve();
});

View File

@ -1,5 +1,5 @@
import {HtmlUtils} from "./HtmlUtils";
import {mediaManager, ReportCallback} from "./MediaManager";
import {mediaManager, ReportCallback, ShowReportCallBack} from "./MediaManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
@ -61,7 +61,7 @@ export class DiscussionManager {
const inputMessage: HTMLInputElement = document.createElement('input');
inputMessage.onfocus = () => {
if(this.userInputManager) {
this.userInputManager.clearAllInputKeyboard();
this.userInputManager.clearAllKeys();
}
}
inputMessage.onblur = () => {
@ -99,7 +99,7 @@ export class DiscussionManager {
name: string|undefined,
img?: string|undefined,
isMe: boolean = false,
reportCallback?: ReportCallback
showReportCallBack?: ShowReportCallBack
) {
const divParticipant: HTMLDivElement = document.createElement('div');
divParticipant.classList.add('participant');
@ -128,8 +128,8 @@ export class DiscussionManager {
reportBanUserAction.classList.add('report-btn')
reportBanUserAction.innerText = 'Report';
reportBanUserAction.addEventListener('click', () => {
if(reportCallback) {
mediaManager.showReportModal(`${userId}`, name ?? '', reportCallback);
if(showReportCallBack) {
showReportCallBack(`${userId}`, name);
}else{
console.info('report feature is not activated!');
}

View File

@ -1,27 +1,45 @@
export class HtmlUtils {
public static getElementByIdOrFail<T extends HTMLElement>(id: string): T {
const elem = document.getElementById(id);
if (elem === null) {
throw new Error("Cannot find HTML element with id '"+id+"'");
if (HtmlUtils.isHtmlElement<T>(elem)) {
return elem;
}
// FIXME: does not check the type of the returned type
return elem as T;
throw new Error("Cannot find HTML element with id '"+id+"'");
}
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
const elem = document.querySelector<T>(selector);
if (HtmlUtils.isHtmlElement<T>(elem)) {
return elem;
}
throw new Error("Cannot find HTML element with selector '"+selector+"'");
}
public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T {
const elem = document.getElementById(id);
if (elem === null) {
throw new Error("Cannot find HTML element with id '"+id+"'");
if (HtmlUtils.isHtmlElement<T>(elem)) {
elem.remove();
return elem;
}
// FIXME: does not check the type of the returned type
elem.remove();
return elem as T;
throw new Error("Cannot find HTML element with id '"+id+"'");
}
private static escapeHtml(html: string): string {
const text = document.createTextNode(html);
const p = document.createElement('p');
p.appendChild(text);
return p.innerHTML;
}
public static urlify(text: string): string {
const urlRegex = /(https?:\/\/[^\s]+)/g;
text = HtmlUtils.escapeHtml(text);
return text.replace(urlRegex, (url: string) => {
return '<a href="' + url + '" target="_blank" style=":visited {color: white}">' + url + '</a>';
})
});
}
private static isHtmlElement<T extends HTMLElement>(elem: HTMLElement | null): elem is T {
return elem !== null;
}
}

View File

@ -3,11 +3,21 @@ import {mediaManager} from "./MediaManager";
import {coWebsiteManager} from "./CoWebsiteManager";
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
const interfaceConfig = {
const getDefaultConfig = () => {
return {
startWithAudioMuted: !mediaManager.constraintsMedia.audio,
startWithVideoMuted: mediaManager.constraintsMedia.video === false,
prejoinPageEnabled: false
}
}
const defaultInterfaceConfig = {
SHOW_CHROME_EXTENSION_BANNER: false,
MOBILE_APP_PROMO: false,
HIDE_INVITE_MORE_HEADER: true,
DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
DISABLE_VIDEO_BACKGROUND: true,
// Note: hiding brand does not seem to work, we probably need to put this on the server side.
SHOW_BRAND_WATERMARK: false,
@ -25,13 +35,40 @@ const interfaceConfig = {
],
};
const slugify = (...args: (string | number)[]): string => {
const value = args.join(' ')
return value
.normalize('NFD') // split an accented letter in the base letter and the accent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, '-') // separator
}
class JitsiFactory {
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this);
public start(roomName: string, playerName:string, jwt?: string): void {
/**
* Slugifies the room name and prepends the room name with the instance
*/
public getRoomName(roomName: string, instance: string): string {
return slugify(instance.replace('/', '-') + "-" + roomName);
}
public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object): void {
coWebsiteManager.insertCoWebsite((cowebsiteDiv => {
// Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
// after some time the URLs get so huge that loading the iframe
// becomes slow and eventually breaks completely. Thus lets just
// clear jitsi local storage before starting a new conference.
window.localStorage.removeItem("jitsiLocalStorage");
const domain = JITSI_URL;
const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any
roomName: roomName,
@ -39,17 +76,13 @@ class JitsiFactory {
width: "100%",
height: "100%",
parentNode: cowebsiteDiv,
configOverwrite: {
startWithAudioMuted: !mediaManager.constraintsMedia.audio,
startWithVideoMuted: mediaManager.constraintsMedia.video === false,
prejoinPageEnabled: false
},
interfaceConfigOverwrite: interfaceConfig,
configOverwrite: {...config, ...getDefaultConfig()},
interfaceConfigOverwrite: {...defaultInterfaceConfig, ...interfaceConfig}
};
if (!options.jwt) {
delete options.jwt;
}
return new Promise((resolve, reject) => {
options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => resolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load
@ -87,7 +120,6 @@ class JitsiFactory {
mediaManager.enableCamera();
}
}
}
export const jitsiFactory = new JitsiFactory();
export const jitsiFactory = new JitsiFactory();

View File

@ -218,7 +218,7 @@ class LayoutManager {
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
*/
public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} {
const game = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game');
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas');
if (this.mode === LayoutMode.VideoChat) {
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
const htmlChildren = Array.from(children.values());

View File

@ -3,6 +3,7 @@ import {HtmlUtils} from "./HtmlUtils";
import {discussionManager, SendMessageCallback} from "./DiscussionManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {VIDEO_QUALITY_SELECT} from "../Administration/ConsoleGlobalMessageManager";
import {UserSimplePeerInterface} from "./SimplePeer";
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
const localValueVideo = localStorage.getItem(VIDEO_QUALITY_SELECT);
@ -23,9 +24,9 @@ export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void;
export type ReportCallback = (message: string) => void;
export type ShowReportCallBack = (userId: string, userName: string|undefined) => void;
// TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only)
// TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!)
export class MediaManager {
localStream: MediaStream|null = null;
localScreenCapture: MediaStream|null = null;
@ -46,6 +47,7 @@ export class MediaManager {
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
showReportModalCallBacks : Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
private microphoneBtn: HTMLDivElement;
private cinemaBtn: HTMLDivElement;
private monitorBtn: HTMLDivElement;
@ -469,8 +471,9 @@ export class MediaManager {
return this.getCamera();
}
addActiveVideo(userId: string, reportCallBack: ReportCallback|undefined, userName: string = ""){
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
this.webrtcInAudio.play();
const userId = ''+user.userId
userName = userName.toUpperCase();
const color = this.getColorByString(userName);
@ -480,33 +483,39 @@ export class MediaManager {
<div class="connecting-spinner"></div>
<div class="rtc-error" style="display: none"></div>
<i id="name-${userId}" style="background-color: ${color};">${userName}</i>
<img id="microphone-${userId}" src="resources/logos/microphone-close.svg">
` +
((reportCallBack!==undefined)?`<img id="report-${userId}" class="report active" src="resources/logos/report.svg">`:'')
+
`<video id="${userId}" autoplay></video>
<img id="microphone-${userId}" title="mute" src="resources/logos/microphone-close.svg">
<button id="report-${userId}" class="report">
<img title="report this user" src="resources/logos/report.svg">
<span>Report/Block</span>
</button>
<video id="${userId}" autoplay></video>
<img src="resources/logos/blockSign.svg" id="blocking-${userId}" class="block-logo">
</div>
`;
layoutManager.add(DivImportance.Normal, userId, html);
if (reportCallBack) {
const reportBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(`report-${userId}`);
reportBtn.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.showReportModal(userId, userName, reportCallBack);
});
}
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
//permit to create participant in discussion part
this.addNewParticipant(userId, userName, undefined, reportCallBack);
const showReportUser = () => {
for(const callBack of this.showReportModalCallBacks){
callBack(userId, userName);
}
};
this.addNewParticipant(userId, userName, undefined, showReportUser);
const reportBanUserActionEl: HTMLImageElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>(`report-${userId}`);
reportBanUserActionEl.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showReportUser();
});
}
addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){
userId = `screen-sharing-${userId}`;
userId = this.getScreenSharingId(userId);
const html = `
<div id="div-${userId}" class="video-container">
<video id="${userId}" autoplay></video>
@ -517,7 +526,11 @@ export class MediaManager {
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
}
private getScreenSharingId(userId: string): string {
return `screen-sharing-${userId}`;
}
disabledMicrophoneByUserId(userId: number){
const element = document.getElementById(`microphone-${userId}`);
if(!element){
@ -556,6 +569,10 @@ export class MediaManager {
}
}
toggleBlockLogo(userId: number, show: boolean): void {
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-'+userId);
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
}
addStreamRemoteVideo(userId: string, stream : MediaStream): void {
const remoteVideo = this.remoteVideo.get(userId);
if (remoteVideo === undefined) {
@ -565,12 +582,12 @@ export class MediaManager {
}
addStreamRemoteScreenSharing(userId: string, stream : MediaStream){
// In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet
const remoteVideo = this.remoteVideo.get(`screen-sharing-${userId}`);
const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId));
if (remoteVideo === undefined) {
this.addScreenSharingActiveVideo(userId);
}
this.addStreamRemoteVideo(`screen-sharing-${userId}`, stream);
this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream);
}
removeActiveVideo(userId: string){
@ -581,7 +598,7 @@ export class MediaManager {
this.removeParticipant(userId);
}
removeActiveScreenSharingVideo(userId: string) {
this.removeActiveVideo(`screen-sharing-${userId}`)
this.removeActiveVideo(this.getScreenSharingId(userId))
}
playWebrtcOutSound(): void {
@ -617,7 +634,7 @@ export class MediaManager {
errorDiv.style.display = 'block';
}
isErrorScreenSharing(userId: string): void {
this.isError(`screen-sharing-${userId}`);
this.isError(this.getScreenSharingId(userId));
}
@ -645,65 +662,8 @@ export class MediaManager {
return color;
}
public showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){
//create report text area
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
const divReport = document.createElement('div');
divReport.classList.add('modal-report-user');
const inputHidden = document.createElement('input');
inputHidden.id = 'input-report-user';
inputHidden.type = 'hidden';
inputHidden.value = userId;
divReport.appendChild(inputHidden);
const titleMessage = document.createElement('p');
titleMessage.id = 'title-report-user';
titleMessage.innerText = 'Open a report';
divReport.appendChild(titleMessage);
const bodyMessage = document.createElement('p');
bodyMessage.id = 'body-report-user';
bodyMessage.innerText = `You are about to open a report regarding an offensive conduct from user ${userName.toUpperCase()}. Please explain to us how you think ${userName.toUpperCase()} breached the code of conduct.`;
divReport.appendChild(bodyMessage);
const imgReportUser = document.createElement('img');
imgReportUser.id = 'img-report-user';
imgReportUser.src = 'resources/logos/report.svg';
divReport.appendChild(imgReportUser);
const textareaUser = document.createElement('textarea');
textareaUser.id = 'textarea-report-user';
textareaUser.placeholder = 'Write ...';
divReport.appendChild(textareaUser);
const buttonReport = document.createElement('button');
buttonReport.id = 'button-save-report-user';
buttonReport.innerText = 'Report';
buttonReport.addEventListener('click', () => {
if(!textareaUser.value){
textareaUser.style.border = '1px solid red'
return;
}
reportCallBack(textareaUser.value);
divReport.remove();
});
divReport.appendChild(buttonReport);
const buttonCancel = document.createElement('img');
buttonCancel.id = 'cancel-report-user';
buttonCancel.src = 'resources/logos/close.svg';
buttonCancel.addEventListener('click', () => {
divReport.remove();
});
divReport.appendChild(buttonCancel);
mainContainer.appendChild(divReport);
}
public addNewParticipant(userId: number|string, name: string|undefined, img?: string, reportCallBack?: ReportCallback){
discussionManager.addParticipant(userId, name, img, false, reportCallBack);
public addNewParticipant(userId: number|string, name: string|undefined, img?: string, showReportUserCallBack?: ShowReportCallBack){
discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack);
}
public removeParticipant(userId: number|string){
@ -769,6 +729,10 @@ export class MediaManager {
this.checkActiveUser();
}, this.focused ? 10000 : 1000);
}
public setShowReportModalCallBacks(callback: ShowReportCallBack){
this.showReportModalCallBacks.add(callback);
}
}
export const mediaManager = new MediaManager();

View File

@ -1,8 +1,9 @@
import * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager";
import {TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection";
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
import {UserSimplePeerInterface} from "./SimplePeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
@ -16,25 +17,28 @@ export class ScreenSharingPeer extends Peer {
private isReceivingStream:boolean = false;
public toClose: boolean = false;
public _connected: boolean = false;
private userId: number;
constructor(private userId: number, initiator: boolean, private connection: RoomConnection) {
constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection) {
super({
initiator: initiator ? initiator : false,
reconnectTimer: 10000,
config: {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
urls: STUN_SERVER.split(',')
},
{
TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','),
username: TURN_USER,
credential: TURN_PASSWORD
},
]
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD
} : undefined,
].filter((value) => value !== undefined)
}
});
this.userId = user.userId;
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
this.sendWebrtcScreenSharingSignal(data);

View File

@ -9,13 +9,18 @@ import {
UpdatedLocalStreamCallback
} from "./MediaManager";
import {ScreenSharingPeer} from "./ScreenSharingPeer";
import {MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
import {RoomConnection} from "../Connexion/RoomConnection";
import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
import {blackListManager} from "./BlackListManager";
export interface UserSimplePeerInterface{
userId: number;
name?: string;
initiator?: boolean;
webRtcUser?: string|undefined;
webRtcPassword?: string|undefined;
}
export interface PeerConnectionListener {
@ -36,6 +41,7 @@ export class SimplePeer {
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number;
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
// We need to go through this weird bound function pointer in order to be able to "free" this reference later.
@ -46,6 +52,7 @@ export class SimplePeer {
mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback);
mediaManager.onStartScreenSharing(this.sendLocalScreenSharingStreamCallback);
mediaManager.onStopScreenSharing(this.stopLocalScreenSharingStreamCallback);
this.userId = Connection.getUserId();
this.initialise();
}
@ -89,15 +96,14 @@ export class SimplePeer {
});
}
private receiveWebrtcStart(user: UserSimplePeerInterface) {
//this.WebRtcRoomId = data.roomId;
private receiveWebrtcStart(user: UserSimplePeerInterface): void {
this.Users.push(user);
// Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group)
// So we can receive a request we already had before. (which will abort at the first line of createPeerConnection)
// This would be symmetrical to the way we handle disconnection.
//start connection
console.log('receiveWebrtcStart. Initiator: ', user.initiator)
//console.log('receiveWebrtcStart. Initiator: ', user.initiator)
if(!user.initiator){
return;
}
@ -134,17 +140,13 @@ export class SimplePeer {
mediaManager.removeActiveVideo("" + user.userId);
const reportCallback = this.enableReporting ? (comment: string) => {
this.reportUser(user.userId, comment);
} : undefined;
mediaManager.addActiveVideo(user, name);
mediaManager.addActiveVideo("" + user.userId, reportCallback, name);
const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection);
//permit to send message
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), message: message})));
peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), userId: this.userId, message: message})));
});
peer.toClose = false;
@ -189,7 +191,7 @@ export class SimplePeer {
mediaManager.addScreenSharingActiveVideo("" + user.userId);
}
const peer = new ScreenSharingPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection);
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {
@ -300,6 +302,7 @@ export class SimplePeer {
}
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
if (blackListManager.isBlackListed(data.userId)) return;
console.log("receiveWebrtcScreenSharingSignal", data);
try {
//if offer type, create peer connection
@ -391,14 +394,8 @@ export class SimplePeer {
}
}
/**
* Triggered locally when clicking on the report button
*/
public reportUser(userId: number, message: string) {
this.Connection.emitReportPlayerMessage(userId, message)
}
private sendLocalScreenSharingStreamToUser(userId: number): void {
if (blackListManager.isBlackListed(userId)) return;
// If a connection already exists with user (because it is already sharing a screen with us... let's use this connection)
if (this.PeerScreenSharingConnectionArray.has(userId)) {
this.pushScreenSharingToRemoteUser(userId);

View File

@ -1,65 +1,57 @@
import * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager";
import {TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection";
import {blackListManager} from "./BlackListManager";
import {Subscription} from "rxjs";
import {UserSimplePeerInterface} from "./SimplePeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
export const MESSAGE_TYPE_CONSTRAINT = 'constraint';
export const MESSAGE_TYPE_MESSAGE = 'message';
export const MESSAGE_TYPE_BLOCKED = 'blocked';
export const MESSAGE_TYPE_UNBLOCKED = 'unblocked';
/**
* A peer connection used to transmit video / audio signals between 2 peers.
*/
export class VideoPeer extends Peer {
public toClose: boolean = false;
public _connected: boolean = false;
private remoteStream!: MediaStream;
private blocked: boolean = false;
private userId: number;
private userName: string;
private onBlockSubscribe: Subscription;
private onUnBlockSubscribe: Subscription;
constructor(public userId: number, initiator: boolean, private connection: RoomConnection) {
constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection) {
super({
initiator: initiator ? initiator : false,
reconnectTimer: 10000,
config: {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
urls: STUN_SERVER.split(',')
},
{
TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','),
username: TURN_USER,
credential: TURN_PASSWORD
},
]
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD
} : undefined,
].filter((value) => value !== undefined)
}
});
console.log('PEER SETUP ', {
initiator: initiator ? initiator : false,
reconnectTimer: 10000,
config: {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: TURN_SERVER.split(','),
username: TURN_USER,
credential: TURN_PASSWORD
},
]
}
});
this.userId = user.userId;
this.userName = user.name || '';
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
this.sendWebrtcSignal(data);
});
this.on('stream', (stream: MediaStream) => {
this.stream(stream);
});
/*peer.on('track', (track: MediaStreamTrack, stream: MediaStream) => {
});*/
this.on('stream', (stream: MediaStream) => this.stream(stream));
this.on('close', () => {
this._connected = false;
@ -70,7 +62,7 @@ export class VideoPeer extends Peer {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => {
console.error(`error => ${this.userId} => ${err.code}`, err);
mediaManager.isError("" + userId);
mediaManager.isError("" + this.userId);
});
this.on('connect', () => {
@ -81,8 +73,6 @@ export class VideoPeer extends Peer {
this.on('data', (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8'));
console.log("data", message);
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
if (message.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId);
@ -95,8 +85,19 @@ export class VideoPeer extends Peer {
} else {
mediaManager.disabledVideoByUserId(this.userId);
}
} else if(message.type === 'message') {
mediaManager.addNewMessage(message.name, message.message);
} else if(message.type === MESSAGE_TYPE_MESSAGE) {
if (!blackListManager.isBlackListed(message.userId)) {
mediaManager.addNewMessage(message.name, message.message);
}
} else if(message.type === MESSAGE_TYPE_BLOCKED) {
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
// Find a way to block A's output stream in A's js client
//However, the output stream stream B is correctly blocked in A client
this.blocked = true;
this.toggleRemoteStream(false);
} else if(message.type === MESSAGE_TYPE_UNBLOCKED) {
this.blocked = false;
this.toggleRemoteStream(true);
}
});
@ -105,6 +106,31 @@ export class VideoPeer extends Peer {
});
this.pushVideoToRemoteUser();
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userId) => {
if (userId === this.userId) {
this.toggleRemoteStream(false);
this.sendBlockMessage(true);
}
});
this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userId) => {
if (userId === this.userId) {
this.toggleRemoteStream(true);
this.sendBlockMessage(false);
}
});
if (blackListManager.isBlackListed(this.userId)) {
this.sendBlockMessage(true)
}
}
private sendBlockMessage(blocking: boolean) {
this.write(new Buffer(JSON.stringify({type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED, name: this.userName.toUpperCase(), userId: this.userId, message: ''})));
}
private toggleRemoteStream(enable: boolean) {
this.remoteStream.getTracks().forEach(track => track.enabled = enable);
mediaManager.toggleBlockLogo(this.userId, !enable);
}
private sendWebrtcSignal(data: unknown) {
@ -120,13 +146,13 @@ export class VideoPeer extends Peer {
*/
private stream(stream: MediaStream) {
try {
this.remoteStream = stream;
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
this.toggleRemoteStream(false);
}
mediaManager.addStreamRemoteVideo("" + this.userId, stream);
}catch (err){
console.error(err);
//Force add streem video
/*setTimeout(() => {
this.stream(stream);
}, 500);*/ //todo: find a way to prevent infinite regression.
}
}
@ -139,6 +165,8 @@ export class VideoPeer extends Peer {
if(!this.toClose){
return;
}
this.onBlockSubscribe.unsubscribe();
this.onUnBlockSubscribe.unsubscribe();
mediaManager.removeActiveVideo("" + this.userId);
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.

View File

@ -1,7 +1,6 @@
import 'phaser';
import GameConfig = Phaser.Types.Core.GameConfig;
import {DEBUG_MODE, JITSI_URL, RESOLUTION} from "./Enum/EnvironmentVariable";
import {cypressAsserter} from "./Cypress/CypressAsserter";
import {LoginScene} from "./Phaser/Login/LoginScene";
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene";
@ -53,8 +52,28 @@ const fps : Phaser.Types.Core.FPSConfig = {
smoothStep: false
}
// the ?phaserMode=canvas parameter can be used to force Canvas usage
const params = new URLSearchParams(document.location.search.substring(1));
const phaserMode = params.get("phaserMode");
let mode: number;
switch (phaserMode) {
case 'auto':
case null:
mode = Phaser.AUTO;
break;
case 'canvas':
mode = Phaser.CANVAS;
break;
case 'webgl':
mode = Phaser.WEBGL;
break;
default:
throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"');
}
const config: GameConfig = {
type: Phaser.AUTO,
type: mode,
title: "WorkAdventure",
width: width / RESOLUTION,
height: height / RESOLUTION,
@ -65,6 +84,11 @@ const config: GameConfig = {
dom: {
createContainer: true
},
render: {
pixelArt: true,
roundPixels: true,
antialias: false
},
physics: {
default: "arcade",
arcade: {
@ -73,16 +97,15 @@ const config: GameConfig = {
},
callbacks: {
postBoot: game => {
const renderer = game.renderer;
// Commented out to try to fix MacOS bug
/*const renderer = game.renderer;
if (renderer instanceof WebGLRenderer) {
renderer.pipelines.add(OutlinePipeline.KEY, new OutlinePipeline(game));
}
}*/
}
}
};
cypressAsserter.gameStarted();
const game = new Phaser.Game(config);
window.addEventListener('resize', function (event) {

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -x
set -o nounset errexit
template_file_index=dist/index.html.tmpl
template_file_index=dist/index.tmpl.html
generated_file_index=dist/index.html
tmp_trackcodefile=/tmp/trackcode

View File

@ -2,13 +2,19 @@ import "jasmine";
import {HtmlUtils} from "../../../src/WebRtc/HtmlUtils";
describe("urlify()", () => {
it("should transform an url into a link", () => {
const text = HtmlUtils.urlify('https://google.com');
expect(text).toEqual('<a href="https://google.com" target="_blank" style=":visited {color: white}">https://google.com</a>');
// FIXME: we need to add PhantomJS to have a good mock for "document".
/*it("should transform an url into a link", () => {
const text = HtmlUtils.urlify('foo https://google.com bar');
expect(text).toEqual('foo <a href="https://google.com" target="_blank" style=":visited {color: white}">https://google.com</a> bar');
});
it("should not transform a normal text into a link", () => {
const text = HtmlUtils.urlify('hello');
expect(text).toEqual('hello');
});
});
it("should escape HTML", () => {
const text = HtmlUtils.urlify('<h1>boo</h1>');
expect(text).toEqual('&lt;h1&gt;boo&lt;/h1&gt;');
});*/
});

View File

@ -39,13 +39,34 @@ module.exports = {
plugins: [
new HtmlWebpackPlugin(
{
template: './dist/index.html'
template: './dist/index.tmpl.html',
minify: {
collapseWhitespace: true,
keepClosingSlash: true,
removeComments: false,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
}
}
),
new webpack.ProvidePlugin({
Phaser: 'phaser'
}),
new webpack.EnvironmentPlugin(['API_URL', 'UPLOADER_URL', 'ADMIN_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL', 'JITSI_PRIVATE_MODE', 'START_ROOM_URL'])
new webpack.EnvironmentPlugin([
'API_URL',
'UPLOADER_URL',
'ADMIN_URL',
'DEBUG_MODE',
'STUN_SERVER',
'TURN_SERVER',
'TURN_USER',
'TURN_PASSWORD',
'JITSI_URL',
'JITSI_PRIVATE_MODE',
'START_ROOM_URL'
])
],
};

View File

@ -1771,7 +1771,7 @@ eventemitter3@^2.0.3:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=
eventemitter3@^4.0.0, eventemitter3@^4.0.7:
eventemitter3@^4.0.0, eventemitter3@^4.0.4:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
@ -1829,7 +1829,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
exports-loader@^1.1.1:
exports-loader@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-1.1.1.tgz#88c9a6877ee6a5519d7c41a016bdd99148421e69"
integrity sha512-CmyhIR2sJ3KOfVsHjsR0Yvo+0lhRhRMAevCbB8dhTVLHsZPs0lCQTvRmR9YNvBXDBxUuhmCE2f54KqEjZUaFrg==
@ -2528,7 +2528,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0"
resolve-cwd "^2.0.0"
imports-loader@^1.2.0:
imports-loader@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-1.2.0.tgz#b06823d0bb42e6f5ff89bc893829000eda46693f"
integrity sha512-zPvangKEgrrPeqeUqH0Uhc59YqK07JqZBi9a9cQ3v/EKUIqrbJHY4CvUrDus2lgQa5AmPyXuGrWP8JJTqzE5RQ==
@ -3663,14 +3663,14 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
phaser@^3.52.0:
version "3.52.0"
resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.52.0.tgz#834dd7a7717308c2576d2450d0806317cf3d1ea8"
integrity sha512-oH39AUXR+cletB3GIvLnVO2J7/M6xs0D0LamVdCrbCDO3jckQOVBcqR6SJ2wam+4D5iJTCXv8K+FmxFzcTUWuA==
phaser@3.24.1:
version "3.24.1"
resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.24.1.tgz#376e0c965d2a35af37c06ee78627dafbde5be017"
integrity sha512-WbrRMkbpEzarkfrq83akeauc6b8xNxsOTpDygyW7wrU2G2ne6kOYu3hji4UAaGnZaOLrVuj8ycYPjX9P1LxcDw==
dependencies:
eventemitter3 "^4.0.7"
exports-loader "^1.1.1"
imports-loader "^1.2.0"
eventemitter3 "^4.0.4"
exports-loader "^1.1.0"
imports-loader "^1.1.0"
path "^0.12.7"
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
@ -4094,7 +4094,7 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
rxjs@^6.6.0:
rxjs@^6.6.0, rxjs@^6.6.3:
version "6.6.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==

4
maps/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
/dist/bundle.js
/yarn-error.log
/Dockerfile

View File

@ -298,7 +298,7 @@
{
"name":"exitUrl",
"type":"string",
"value":"\/@\/tcm\/workadventure\/floor1"
"value":"..\/Floor1\/floor1.json"
}],
"type":"tilelayer",
"visible":true,

View File

@ -83,9 +83,9 @@
"opacity":1,
"properties":[
{
"name":"exitSceneUrl",
"name":"exitUrl",
"type":"string",
"value":"\/@\/tcm\/workadventure\/floor0#down-the-stairs"
"value":"..\/Floor0\/floor0.json"
}],
"type":"tilelayer",
"visible":true,
@ -264,7 +264,7 @@
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.4.2",
"tiledversion":"1.3.3",
"tileheight":32,
"tilesets":[
{
@ -1959,6 +1959,6 @@
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"version":1.2,
"width":46
}

View File

@ -0,0 +1,25 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
- See the file: cc-by-sa-3.0.txt
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
- See the file: gpl-3.0.txt
Assets from: workadventure@thecodingmachine.com
BASE assets:
------------
- le-coq.png
- logotcm.png
- pin.png
- tileset1-repositioning.png
- tileset1.png
- tileset2.2.png
- tileset2.png
- tileset3.2.png
- tileset3.png
- walls2.png

View File

@ -0,0 +1,99 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":4,
"name":"jitsi",
"opacity":1,
"properties":[
{
"name":"jitsiConfig",
"type":"string",
"value":"{ \"startWithAudioMuted\": true }"
},
{
"name":"jitsiInterfaceConfig",
"type":"string",
"value":"{ \"DEFAULT_BACKGROUND\": \"#77ee77\" }"
},
{
"name":"jitsiRoom",
"type":"string",
"value":"myRoom avec espace \u00e9\u00e0$&'\"_ \ud83d\ude00"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.3.3",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.2,
"width":10
}

BIN
maps/tests/tileset1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

4
messages/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
/dist/bundle.js
/yarn-error.log
/Dockerfile

View File

@ -168,6 +168,8 @@ message WebRtcStartMessage {
int32 userId = 1;
string name = 2;
bool initiator = 3;
string webrtcUserName = 4;
string webrtcPassword = 5;
}
message WebRtcDisconnectMessage {
@ -177,6 +179,8 @@ message WebRtcDisconnectMessage {
message WebRtcSignalToClientMessage {
int32 userId = 1;
string signal = 2;
string webrtcUserName = 4;
string webrtcPassword = 5;
}
message TeleportMessageMessage{

View File

@ -1,15 +1,26 @@
FROM thecodingmachine/workadventure-back-base:latest as builder
WORKDIR /var/www/messages
COPY --chown=docker:docker messages .
# protobuf build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
WORKDIR /usr/src
COPY messages .
RUN yarn install && yarn proto
FROM thecodingmachine/nodejs:12
COPY --chown=docker:docker pusher .
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated
# typescript build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
WORKDIR /usr/src
COPY pusher/yarn.lock pusher/package.json ./
RUN yarn install
COPY pusher .
COPY --from=builder /usr/src/generated src/Messages/generated
ENV NODE_ENV=production
RUN yarn run tsc
# final production image
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76
WORKDIR /usr/src
COPY pusher/yarn.lock pusher/package.json ./
COPY --from=builder2 /usr/src/dist /usr/src/dist
ENV NODE_ENV=production
RUN yarn install --production
USER node
CMD ["yarn", "run", "runprod"]

17
pusher/Dockerfile.prod Normal file
View File

@ -0,0 +1,17 @@
FROM node:12.19.0-slim
RUN mkdir -p /home/node/app && chown -R node:node /home/node/app
WORKDIR /home/node/app
USER node
ENV NODE_ENV=production
ENV DEBUG=*
COPY --chown=node:node package.json yarn.lock ./
RUN yarn install --prod --frozen-lockfile
COPY --chown=node:node ./dist/ ./dist/
EXPOSE 8080
CMD ["yarn", "run", "runprod"]

View File

@ -105,11 +105,12 @@ class AdminApi {
return res.data;
}
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) {
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) {
return Axios.post(`${ADMIN_API_URL}/api/report`, {
reportedUserUuid,
reportedUserComment,
reporterUserUuid,
reportWorldSlug
},
{
headers: {"Authorization": `${ADMIN_API_TOKEN}`}

View File

@ -172,6 +172,7 @@ export class SocketManager implements ZoneEventListener {
console.log('Calling joinRoom')
const apiClient = await apiClientRepository.getClient(client.roomId);
const streamToPusher = apiClient.joinRoom();
clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId);
client.backConnection = streamToPusher;
@ -304,7 +305,7 @@ export class SocketManager implements ZoneEventListener {
throw 'reported socket user not found';
}
//TODO report user on admin application
await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid)
await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2])
} catch (e) {
console.error('An error occurred on "handleReportMessage"');
console.error(e);

View File

@ -1,9 +1,19 @@
FROM thecodingmachine/nodejs:12
COPY --chown=docker:docker uploader .
# typescript build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
WORKDIR /usr/src
COPY uploader/yarn.lock uploader/package.json ./
RUN yarn install
COPY uploader .
ENV NODE_ENV=production
RUN yarn run tsc
# final production image
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76
WORKDIR /usr/src
COPY uploader/yarn.lock uploader/package.json ./
COPY --from=builder2 /usr/src/dist /usr/src/dist
ENV NODE_ENV=production
RUN yarn install --production
USER node
CMD ["yarn", "run", "runprod"]

5
website/.dockerignore Normal file
View File

@ -0,0 +1,5 @@
/dist/
/node_modules/
/dist/bundle.js
/yarn-error.log
/Dockerfile